TSone

Component System

TSone's component system lets you split UI into independent, reusable class components.

Component Definition

Define a component by extending Component<Props, State> and implementing initState(), initStyles(), and protected render(): VNode.

Basic Component Structure

import { Component, VNode } from '@geektech/tsone';

interface MessageState {
  message: string;
}

class MyComponent extends Component<object, MessageState> {
  protected initState(): MessageState {
    return { message: 'Hello, TSone!' };
  }

  protected initStyles(): void {
    this.styleManager.addStyle('.my-component', {
      selector: '.my-component',
      properties: {
        color: '#333',
        fontSize: '16px',
        padding: '10px',
      },
    });
  }

  protected render(): VNode {
    return {
      tag: 'div',
      props: { className: 'my-component' },
      children: [''],
    };
  }
}

Component Lifecycle

Mounting Phase

  • beforeMount(): called before the component is mounted in the DOM
  • onMounted(): called after the component is mounted in the DOM

Update Phase

  • beforeUpdate(): called before component state updates
  • onUpdated(): called after component state updates

Unmounting Phase

  • beforeUnmount(): called before the component is removed from the DOM
  • onUnmounted(): called after the component is removed from the DOM

Error Handling (Error Boundaries)

Use onErrorCaptured(error, instance) to catch errors thrown while a descendant component renders or updates. Return false to mark the error as handled and stop it from propagating; return true or undefined to let it bubble up to an outer boundary.

class ErrorBoundary extends Component<object, { failed: boolean }> {
  protected initState() {
    return { failed: false };
  }

  protected initStyles(): void {}

  protected onErrorCaptured(): boolean {
    this.state.failed = true;
    return false;
  }

  protected render(): VNode {
    if (this.state.failed) {
      return { tag: 'p', children: ['A child failed; the boundary took over.'] };
    }
    return { tag: 'div', children: this.props.children ?? [] };
  }
}

Lifecycle Example

class LifecycleComponent extends Component<object, { count: number }> {
  protected initState() {
    return { count: 0 };
  }

  protected initStyles(): void {}

  protected beforeMount() {
    console.log('Component will mount');
  }

  protected onMounted() {
    console.log('Component mounted');
  }

  protected beforeUpdate() {
    console.log('Component will update');
  }

  protected onUpdated() {
    console.log('Component updated');
  }

  protected render(): VNode {
    return {
      tag: 'button',
      children: ['Count: '],
      listeners: {
        click: () => this.state.count++,
      },
    };
  }
}

Component Props

Define a Props Interface

interface ButtonProps {
  text: string;
  disabled?: boolean;
  size?: 'small' | 'medium' | 'large';
}

class Button extends Component<ButtonProps, object> {
  protected initState() {
    return {};
  }

  protected initStyles(): void {}

  protected render(): VNode {
    const { text, disabled = false, size = 'medium' } = this.props;
    return {
      tag: 'button',
      props: {
        className: `btn btn-${size}` ,
        disabled,
      },
      children: [text],
    };
  }
}

Use Component Props

class ParentComponent extends Component<object, object> {
  protected initState() {
    return {};
  }

  protected initStyles(): void {}

  protected render(): VNode {
    return {
      tag: 'div',
      children: [
        {
          component: Button,
          props: { text: 'Click Me', size: 'large' },
        },
      ],
    };
  }
}

Component Events

Emit an Event

class CounterComponent extends Component<object, { count: number }> {
  protected initState() {
    return { count: 0 };
  }

  protected initStyles(): void {}

  protected handleIncrement() {
    this.state.count++;
    this.emit('increment', this.state.count);
  }

  protected render(): VNode {
    return {
      tag: 'button',
      listeners: { click: () => this.handleIncrement() },
      children: ['+'],
    };
  }
}

Listen for an Event

class ParentCounter extends Component<object, object> {
  protected initState() {
    return {};
  }

  protected initStyles(): void {}

  protected handleCounterChange(value: number) {
    console.log('Counter changed:', value);
  }

  protected render(): VNode {
    return {
      tag: 'div',
      children: [
        {
          component: CounterComponent,
          listeners: {
            increment: (value: number) => this.handleCounterChange(value),
          },
        },
      ],
    };
  }
}

Component Nesting

class App extends Component<object, object> {
  protected initState() {
    return {};
  }

  protected initStyles(): void {}

  protected render(): VNode {
    return {
      tag: 'div',
      props: { className: 'app' },
      children: [
        { tag: 'header', children: [{ component: NavigationComponent }] },
        { tag: 'main', children: [{ component: HomeComponent }] },
      ],
    };
  }
}

Component State Management

Initialize State

protected initState() {
  return {
    count: 0,
    user: { name: 'John', age: 30 },
    items: ['Item 1', 'Item 2', 'Item 3'],
  };
}

Update State

this.state.count = 1;
this.state.user.name = 'Jane';
this.state.items.push('Item 4');

State changes are merged through the scheduler and flushed on a microtask: a burst of synchronous mutations triggers a single re-render. Use flushSync() to read the latest DOM synchronously, or await nextTick() in async code.

Component Style Management

Each component instance owns its own styleManager.

Add Styles

protected initStyles() {
  this.styleManager.addStyle('.button', {
    selector: '.button',
    properties: {
      padding: '8px 16px',
      backgroundColor: '#007bff',
      color: '#fff',
    },
  });

  this.styleManager.addStyle('.button:hover', {
    selector: '.button:hover',
    properties: { backgroundColor: '#0069d9' },
  });
}

Remove Styles

this.styleManager.removeStyle('.button');
this.styleManager.clearStyles();

Transitions and Component Caching

Transition plays an enter/leave transition for a single element while show toggles: on enter it applies {name}-enter-from / {name}-enter-to (each with an -active class), on leave {name}-leave-from / {name}-leave-to, and the element is removed duration ms after leave starts. Default name is transition and duration is 300.

import { Transition } from '@geektech/tsone';

{
  component: Transition,
  props: { show: this.state.open, name: 'fade', duration: 300 },
  children: [{ tag: 'div', children: ['content'] }],
}

KeepAlive keeps every keyed child component mounted: when activeKey changes, inactive children stay hidden via display:none with state and DOM preserved, which fits tab switching and other state-preserving scenarios.

import { KeepAlive } from '@geektech/tsone';

{
  component: KeepAlive,
  props: { activeKey: this.state.activeTab },
  children: [
    { key: 'tab-a', component: PanelA },
    { key: 'tab-b', component: PanelB },
  ],
}

Component Context

Components can read global configuration through application context and access the router through the router getter.

Get Context

const context = this.getContext();
const router = this.router;

router?.push('/about');
console.log(context?.config);

Best Practices

Component Design Principles

  • Single responsibility: each component should handle one concern
  • Reusability: design general-purpose, reusable components
  • Maintainability: keep component code concise and clear
  • Performance: avoid unnecessary rendering and computation

Component Naming Conventions

  • Use PascalCase for component class names
  • Follow the repository's existing naming style for component files
  • Use kebab-case for component style class names

Performance Tips

  • Avoid creating unnecessary objects in render
  • Keep only properties requiring reactive updates in state
  • Use computed for complex derived values
  • Keep component nesting shallow

Summary

With class components, lifecycle hooks, events, slots, and local style management, TSone maintains a clear object-oriented component model.