TSone

组件 API

TSone 使用面向对象组件模型。每个组件继承 Component<Props, State>,并通过 protected render(): VNode 返回 VNode。

Component<Props, State>

名称签名说明
Component<Props, State>abstract class Component<Props, State>基于类的组件基类,提供 props、state、生命周期和 render。
import { Component, VNode } from '@geektech/tsone';

interface CounterProps {
  initial?: number;
}

interface CounterState {
  count: number;
}

class Counter extends Component<CounterProps, CounterState> {
  protected initState(): CounterState {
    return {
      count: this.props.initial ?? 0,
    };
  }

  protected initStyles(): void {}

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

必须实现的方法

  • protected initState(): State: 返回组件初始状态,状态会被 reactive 包装
  • protected initStyles(): void: 初始化组件样式,可使用 this.styleManager
  • protected render(): VNode: 返回虚拟节点

生命周期

  • beforeMount(): 首次渲染前
  • onMounted(): DOM 挂载后
  • beforeUpdate(): 状态更新触发 patch 前
  • onUpdated(): patch 完成后
  • beforeUnmount(): 卸载前
  • onUnmounted(): 卸载后
  • onErrorCaptured(error, instance): 捕获后代组件渲染或更新抛出的错误,返回 false 停止传播

状态和属性

component.setProps({ initial: 3 });
component.setState({ count: 4 });
  • props: 构造组件时传入,也可由父组件 VNode 的 props 提供
  • state: 响应式状态
  • setProps(props): 合并更新属性,已挂载时触发更新
  • setState(state): 合并更新状态

事件

const unsubscribe = component.on('submit', (payload) => {
  console.log(payload);
});

unsubscribe();

组件内部可使用 this.emit(eventName, ...args) 触发事件。

在父组件 VNode 上使用 emitters 可声明事件处理器;patch 时旧处理器会被替换或清理。

{
  component: Editor,
  emitters: {
    saved: (payload) => this.save(payload),
  },
}

依赖注入

使用 InjectionKey、provide 和 inject 在应用和组件树中共享依赖。组件注入按自身、父级、应用顺序查找。

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

const THEME: InjectionKey<{ mode: string }> = Symbol('theme');
app.provide(THEME, { mode: 'dark' });
const theme = this.inject(THEME, { mode: 'light' });

HTML 标签快捷函数

使用 Tag(tag, options) 创建任意 HTML VNode,也可以用具名快捷函数创建常用元素。所有快捷函数都支持相同的 props、children、listeners、key、slot 和 directions 选项。

import { Section, Tag } from '@geektech/tsone';

const panel = Section({ props: { className: 'panel' }, children: ['内容'] });
const dialog = Tag('dialog', { props: { open: true }, children: ['你好'] });

常用快捷函数包括 Div、Span、P、Button、Input、Section、Main、Header、Footer、Nav、Article、Aside、H1 至 H6、Strong、Em、Small、Pre、Code、Blockquote、Ul、Ol、Li、A、Img、Form、Label、Textarea、Select、Option、Table、Thead、Tbody、Tr、Th 和 Td。

条件、列表与模型绑定

import { Input, each } from '@geektech/tsone';

const children = each(
  this.state.users,
  (user) => ({ tag: 'li', children: [user.name] }),
  (user) => user.id
);

const field = Input({
  directions: {
    model: { path: 'profile.name' },
  },
});

const panel = {
  component: ProfilePanel,
  directions: { if: this.state.visible },
};

列表进入和退出动画

TransitionGroup 会为初始和新增的带 key 子节点播放进入动画,并把已移除子节点的卸载延迟到退出动画结束。公开动画类型为 TransitionAnimationType。

import { Li, TransitionGroup, each } from '@geektech/tsone';

{
  component: TransitionGroup,
  props: { tag: 'ul', type: 'fade', duration: 300 },
  children: each(
    this.state.items,
    (item) => Li({ children: [item.label] }),
    (item) => item.id
  ),
}

可用类型包括 fade、slide-up、slide-down、slide-left、slide-right 和 scale。tag 默认 div,type 默认 fade,duration 默认 300 毫秒,缓动固定为 ease。直属子节点必须具有唯一 key。组件遵循 prefers-reduced-motion,Web Animations 不可用时也会跳过动画。重排只移动保留节点,不播放重排或 FLIP 动画。

单元素过渡与组件缓存

Transition 在 show 切换时为单个元素播放过渡,应用 {name}-enter-from / {name}-enter-to 与 {name}-leave-from / {name}-leave-to CSS class(各配套一个 -active class),并在离开开始 duration 毫秒后移除元素。KeepAlive 让所有带 key 的子组件保持挂载(状态与 DOM 保留),仅活跃项可见,适合 tab 切换等需要保留状态的场景。

import { KeepAlive, Transition, createComponent } from '@geektech/tsone';

const dialog = {
  component: Transition,
  props: { show: this.state.open, name: 'fade', duration: 300 },
  children: [Dialog({ children: ['设置'] })],
};

const tabs = {
  component: KeepAlive,
  props: { activeKey: this.state.activeTab },
  children: [
    createComponent(Editor, {}, [], 'editor'),
    createComponent(Preview, {}, [], 'preview'),
  ],
};

表单校验

import { createForm, minLength, required, validate } from '@geektech/tsone';

const form = createForm(this.state, {
  'profile.name': [required('请输入姓名'), minLength(2)],
  'profile.age': [validate((value) => Number(value) >= 18 || '年龄须不小于 18')],
});

const result = form.validate();
const field = form.validateField('profile.name');
form.resetErrors();

Slots

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

  protected initStyles(): void {}

  protected render(): VNode {
    return {
      tag: 'section',
      children: [
        { tag: 'slot', props: { name: 'header' } },
        { tag: 'slot', props: { name: 'default' } },
      ],
    };
  }
}