Files
UI/packages/core/frame/src/dom.ts
T
shawn 0de9b6e66e init: @ui-arch 私人组件库 monorepo
- 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)
2026-10-23 14:58:53 +08:00

71 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}