Skip to content

Latest commit

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

@zogjs/component

A powerful component system plugin for Zog.js that enables you to create reusable, encapsulated UI components with props, events, slots, and scoped reactivity.

Installation

npm install zogjs @zogjs/component

Quick Start

import{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>

Features

  • Reactive Props: Pass data from parent to child with :prop-name syntax
  • Custom Events: Emit events from child to parent with @event-name handlers
  • 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

Component Structure

Basic Component Definition

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'};}});

Template Requirements

  • 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

Props allow you to pass data from parent to child components.

Passing Props

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>

Receiving Props

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 };}});

Prop Types

  • Reactive props (:prop-name): Update automatically when parent data changes
  • Static props (prop-name): String values that don't change

Events

Components can emit events to communicate with their parent.

Emitting Events

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
};}});

Listening to Events

<z-custom-buttonlabel="Save" @button-clicked="onSave"></z-custom-button>
constapp=createApp(()=>({onSave(event){console.log('Button clicked at:',event.timestamp);}}));

Slots

Slots allow you to pass content from parent to child.

Using Slots

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.

Simple Example

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>

Advanced Example

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>

Best Practices

  1. Single Root Element: Always ensure your template has exactly one root element.

  2. 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}
  3. Event Naming: Use descriptive event names and pass relevant data:

    emit('item-selected',{id: item.id,name: item.name});
  4. Reactivity: Use ref() or reactive() for data that needs to be reactive:

    setup(props){constcount=ref(0);// reactiveconstuser=reactive({name: 'John'});// reactive objectreturn{ count, user };}
  5. Component Names: Use descriptive, hyphenated names:

    registerComponent('user-profile',{ ... });registerComponent('data-table',{ ... });

API Reference

registerComponent(name, definition)

Registers a new component.

Parameters:

  • name (string): Component name (used as <z-name> in templates)
  • definition (object):
    • template (string, required): Component HTML template
    • setup (function, optional): Setup function that receives props and context

Returns: void

Setup Function

setup(props,context)

Parameters:

  • props (object): All props passed to the component
  • context (object):
    • emit(eventName, ...args): Function to emit events to parent

Returns: Object with data and methods for the template

Troubleshooting

Component Not Rendering

  • 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)

Props Not Updating

  • Use :prop-name syntax for reactive props
  • Check that the parent data is reactive (using ref() or reactive())

Events Not Firing

  • 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

Learn More

License

MIT


Made with ❤️ for nothing

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages