Skip to content

zogjs/component

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 

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

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

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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •