Component System - cynchro/OldSchoolFrontFrame GitHub Wiki

Component System

Components are reusable UI pieces with their own state, methods, and template. They share the same API as modules but are not tied to a route.


When to use a component

  • The same UI appears in more than one module (e.g. a modal, a data table, a notification banner)
  • A piece of UI is complex enough to benefit from its own state and lifecycle

Don't create a component for a UI element used only once — just put the HTML inline in the module.


Defining a component

// components/alert/alert.js
import { Component } from '../../framework/component/component.js';

export const Alert = Component({
  state: () => ({
    message: '',
    type: 'info', // 'info' | 'success' | 'error'
    visible: false,
  }),

  template: `
    <div class="alert" data-bind="type">
      <span data-bind="message"></span>
      <button data-click="close">✕</button>
    </div>
  `,

  methods: {
    show(ctx, message, type = 'info') {
      ctx.state.message = message;
      ctx.state.type = type;
      ctx.state.visible = true;
    },
    close(ctx) {
      ctx.state.visible = false;
    },
  },

  mounted(ctx) {
    // Optional setup
  },

  destroyed(ctx) {
    // Optional cleanup
  },
});

Mounting a component

// Inside a module's mounted() hook
import { Alert } from '../../components/alert/alert.js';

defineModule({
  mounted(ctx) {
    const alertRoot = ctx.root.querySelector('#alert-container');
    const alert = Alert.mount(alertRoot);

    // Keep a reference to call methods later
    ctx._alert = alert;
  },

  methods: {
    async save(ctx) {
      try {
        await saveData();
        ctx._alert.methods.show(ctx._alert, 'Saved successfully!', 'success');
      } catch (e) {
        ctx._alert.methods.show(ctx._alert, e.message, 'error');
      }
    },
  },

  destroyed(ctx) {
    ctx._alert.destroy();
  },
});

Passing props

const instance = Alert.mount(root, {
  initialMessage: 'Welcome!',
  type: 'info',
});

Props are available in mounted(ctx) via ctx.props:

mounted(ctx) {
  if (ctx.props.initialMessage) {
    ctx.state.message = ctx.props.initialMessage;
    ctx.state.visible = true;
  }
},

Component instance API

Component(definition).mount(root, props) returns an instance:

Property Description
instance.state Reactive state
instance.methods Bound methods
instance.root DOM root element
instance.destroy() Unmount and clean up

Global component registry

For components used throughout the app, register them globally:

// app.js
import { defineComponent, mountComponent } from './framework/component/component.js';
import { Alert } from './components/alert/alert.js';

defineComponent('alert', Alert);

Then mount by name from any module:

import { mountComponent } from '../../framework/component/component.js';

defineModule({
  mounted(ctx) {
    ctx._alert = mountComponent('alert', ctx.root.querySelector('#alert'));
  },
});

Component folder layout

components/
└── alert/
    ├── alert.js      # Component definition
    ├── alert.css     # Styles (optional)
    └── alert.html    # External template (optional, use template: `` otherwise)
⚠️ **GitHub.com Fallback** ⚠️