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';constapp=createApp(()=>({message: 'Hello from parent'}));// Install the plugin and get access to registerComponentconst{ registerComponent }=app.use(ComponentPlugin);// Register a simple componentregisterComponent('greeting',{template: `<div class="greeting">{{ message }}</div>`,setup(props){return{message: props.message||'Hello!'};}});app.mount('#app');<divid="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 templatereturn{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-buttonlabel="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.initialValueconstcount=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 }){consthandleClick=()=>{emit('button-clicked',{timestamp: Date.now()});};return{label: props.label||'Click',
handleClick
};}});<z-custom-buttonlabel="Save" @button-clicked="onSave"></z-custom-button>constapp=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-cardtitle="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';constapp=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 }){constcount=ref(props.initialValue||0);constincrement=()=>{count.value++;emit('change',count.value);};constdecrement=()=>{count.value--;emit('change',count.value);};return{ count, increment, decrement };}});app.mount('#app');<divid="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';constapp=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 ComponentregisterComponent('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 }){constisEditing=ref(false);consteditText=ref('');consttoggleDone=()=>{emit('toggle',props.todo.id);};constremove=()=>{emit('remove',props.todo.id);};conststartEdit=()=>{isEditing.value=true;editText.value=props.todo.text;};constfinishEdit=()=>{if(editText.value.trim()){emit('edit',{id: props.todo.id,text: editText.value.trim()});}isEditing.value=false;};constcancelEdit=()=>{isEditing.value=false;editText.value=props.todo.text;};return{
isEditing,
editText,
toggleDone,
remove,
startEdit,
finishEdit,
cancelEdit
};}});// Todo List ComponentregisterComponent('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 }){consttodos=ref(props.initialTodos||[]);constnewTodoText=ref('');constfilter=ref('all');constremaining=computed(()=>todos.value.filter(t=>!t.done).length);constcompletedCount=computed(()=>todos.value.filter(t=>t.done).length);constallDone=computed(()=>todos.value.length>0&&remaining.value===0);constfilteredTodos=computed(()=>{if(filter.value==='active'){returntodos.value.filter(t=>!t.done);}if(filter.value==='completed'){returntodos.value.filter(t=>t.done);}returntodos.value;});constaddTodo=()=>{consttext=newTodoText.value.trim();if(text){todos.value.push({id: Date.now(),
text,done: false});newTodoText.value='';emit('change',todos.value);}};consttoggleTodo=(id)=>{consttodo=todos.value.find(t=>t.id===id);if(todo){todo.done=!todo.done;emit('change',todos.value);}};constremoveTodo=(id)=>{constindex=todos.value.findIndex(t=>t.id===id);if(index>-1){todos.value.splice(index,1);emit('change',todos.value);}};consteditTodo=({ id, text })=>{consttodo=todos.value.find(t=>t.id===id);if(todo){todo.text=text;emit('change',todos.value);}};consttoggleAll=()=>{constdone=!allDone.value;todos.value.forEach(todo=>todo.done=done);emit('change',todos.value);};constclearCompleted=()=>{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');<divid="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){constcount=ref(0);// reactiveconstuser=reactive({name: 'John'});// reactive objectreturn{ 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