npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zogjs/component

v0.3.0

Published

ZogJS component plugin

Readme

@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';

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>

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 template
    return {
      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-button label="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.initialValue
    const count = 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 }) {
    const handleClick = () => {
      emit('button-clicked', { timestamp: Date.now() });
    };
    
    return {
      label: props.label || 'Click',
      handleClick
    };
  }
});

Listening to Events

<z-custom-button 
  label="Save" 
  @button-clicked="onSave">
</z-custom-button>
const app = 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-card title="User Profile">
  <p>Name: John Doe</p>
  <p>Email: [email protected]</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';

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>

Advanced Example

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>

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) {
      const count = ref(0); // reactive
      const user = reactive({ name: 'John' }); // reactive object
      return { 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