0de9b6e66e
- core/table-core: Headless 表格内核(排序/筛选/分页/选择插件 + 冒烟 9 项) - core/frame: signal/computed/effect + hyperscript 微框架(零依赖) - skin/ui: 16 个 DOM 组件(含自绘 select、滑动指示条 tabs、行选择 dataTable、form 表单校验) - design/tokens: --ua-* 设计令牌(light/dark) - tools/cli: ua 命令行(index/list/info/docs/style/mcp)+ MCP 注册表服务 - apps: demo 演示站(esbuild 单文件)+ docs 双轨文档(人类版 + llms-full.txt)
71 lines
2.7 KiB
TypeScript
71 lines
2.7 KiB
TypeScript
import { effect } from './signal.js';
|
||
|
||
type Child = Node | string | number | boolean | null | undefined | (() => unknown);
|
||
type Props = Record<string, unknown>;
|
||
|
||
/*** 响应式属性绑定:函数值属性在依赖变化时自动写回 DOM ***/
|
||
function bindProp(el: HTMLElement, key: string, fn: () => unknown): void {
|
||
effect(() => {
|
||
const v = fn();
|
||
if (key === 'value') (el as unknown as HTMLInputElement).value = String(v);
|
||
else if (key === 'checked') (el as unknown as HTMLInputElement).checked = Boolean(v);
|
||
else if (v === false || v == null) el.removeAttribute(key);
|
||
else el.setAttribute(key, v === true ? '' : String(v));
|
||
});
|
||
}
|
||
|
||
/*** 静态属性写入:false / null 移除属性,true 置空标记属性 ***/
|
||
function bindStatic(el: HTMLElement, key: string, value: unknown): void {
|
||
if (value === false || value == null) el.removeAttribute(key);
|
||
else if (value === true) el.setAttribute(key, '');
|
||
else el.setAttribute(key, String(value));
|
||
}
|
||
|
||
/*** 挂载子节点:函数型子节点转为响应式文本节点 ***/
|
||
function appendChild(el: HTMLElement, child: unknown): void {
|
||
if (child == null || child === false) return;
|
||
if (typeof child === 'function') {
|
||
const node = document.createTextNode('');
|
||
effect(() => {
|
||
node.textContent = String(child());
|
||
});
|
||
el.append(node);
|
||
} else if (typeof child === 'object') {
|
||
el.append(child as Node);
|
||
} else {
|
||
el.append(String(child));
|
||
}
|
||
}
|
||
|
||
/*** hyperscript 入口:h(tag, props, ...children);on* 为事件,函数值为响应式属性 ***/
|
||
export function h(tag: string, props?: Props | null, ...children: unknown[]): HTMLElement {
|
||
const el = document.createElement(tag);
|
||
for (const [key, value] of Object.entries(props ?? {})) {
|
||
if (key.startsWith('on') && typeof value === 'function') {
|
||
el.addEventListener(key.slice(2).toLowerCase(), value as EventListener);
|
||
} else if (typeof value === 'function') {
|
||
bindProp(el, key, value as () => unknown);
|
||
} else if (key === 'class') {
|
||
el.className = String(value);
|
||
} else {
|
||
bindStatic(el, key, value);
|
||
}
|
||
}
|
||
for (const child of children.flat(Infinity)) appendChild(el, child);
|
||
return el;
|
||
}
|
||
|
||
/*** 列表渲染:信号变化时整体替换容器子节点(tbody / 列表等场景) ***/
|
||
export function renderList(el: HTMLElement, fn: () => Node[]): void {
|
||
effect(() => {
|
||
el.replaceChildren(...fn());
|
||
});
|
||
}
|
||
|
||
/*** 挂载视图到容器;容器不存在时静默跳过 ***/
|
||
export function mount(target: Element | string, view: () => Node): void {
|
||
const root = typeof target === 'string' ? document.querySelector(target) : target;
|
||
if (!root) return; // 卫语句:容器不存在
|
||
root.replaceChildren(view());
|
||
}
|