weifuwu 0.40.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -83,26 +83,12 @@ function renderValue(v, ctx) {
83
83
  ;
84
84
  el.value = String(selectValue);
85
85
  }
86
+ if (typeof vnode.props?.ref === "function") vnode.props.ref(el);
86
87
  return el;
87
88
  }
88
89
  function renderComponent(Comp, props, vnode, ctx) {
89
- const prev$ = vnode._$;
90
- if (!prev$) vnode._$ = {};
90
+ ;
91
91
  ctx.ui = ctx.ui ?? {};
92
- const _target = vnode._$;
93
- _target._hooks = { mount: [], unmount: [], update: [] };
94
- ctx.ui.onmount = (fn) => {
95
- _target._hooks.mount = [fn];
96
- };
97
- ctx.ui.onunmount = (fn) => {
98
- _target._hooks.unmount = [fn];
99
- };
100
- ctx.ui.onupdate = (fn) => {
101
- _target._hooks.update = [fn];
102
- };
103
- ctx.ui.onmounted = (fn) => {
104
- _target._onmounted = fn;
105
- };
106
92
  let childVNode;
107
93
  try {
108
94
  childVNode = Comp(props, ctx);
@@ -113,10 +99,6 @@ function renderComponent(Comp, props, vnode, ctx) {
113
99
  }
114
100
  vnode._render = childVNode;
115
101
  childVNode = childVNode(props);
116
- if (!prev$) {
117
- const mh = _target._hooks?.mount;
118
- if (mh && mh[0]) mh[0]();
119
- }
120
102
  } catch (e) {
121
103
  const errHandler = ctx.ui?._errorHandler;
122
104
  if (errHandler) {
@@ -132,12 +114,7 @@ function renderComponent(Comp, props, vnode, ctx) {
132
114
  return document.createTextNode("");
133
115
  }
134
116
  vnode._child = childVNode;
135
- const rootNode = renderValue(childVNode, ctx);
136
- if (_target._onmounted && !prev$) {
137
- const cleanup = _target._onmounted(rootNode);
138
- if (typeof cleanup === "function") vnode._cleanup = cleanup;
139
- }
140
- return rootNode;
117
+ return renderValue(childVNode, ctx);
141
118
  }
142
119
  function renderArray(arr, ctx) {
143
120
  const frag = document.createDocumentFragment();
@@ -259,34 +236,8 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
259
236
  const oldV = oldInput;
260
237
  if (typeof newV.type === "function") {
261
238
  const comp = newV.type;
262
- if (oldV._$) newV._$ = oldV._$;
263
- if (!newV._$) newV._$ = {};
264
239
  ctx.ui = ctx.ui ?? {};
265
- const _tgt = newV._$;
266
240
  if (oldV._render) newV._render = oldV._render;
267
- if (oldV._cleanup) newV._cleanup = oldV._cleanup;
268
- if (_tgt._hooks) {
269
- ;
270
- ctx.ui.onmount = (fn) => {
271
- _tgt._hooks.mount = [fn];
272
- };
273
- ctx.ui.onunmount = (fn) => {
274
- _tgt._hooks.unmount = [fn];
275
- };
276
- ctx.ui.onupdate = (fn) => {
277
- _tgt._hooks.update = [fn];
278
- };
279
- }
280
- if (_tgt._onmounted) {
281
- ;
282
- ctx.ui.onmounted = (fn) => {
283
- _tgt._onmounted = fn;
284
- };
285
- }
286
- if (oldV._$) {
287
- const uh = _tgt._hooks?.update;
288
- if (uh && uh[0]) uh[0](oldV.props);
289
- }
290
241
  let childNew;
291
242
  try {
292
243
  if (typeof newV._render === "function") {
@@ -310,10 +261,6 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
310
261
  }
311
262
  const _prevChild = oldV._child;
312
263
  newV._child = childNew;
313
- if (childNew == null && _tgt._hooks?.unmount?.length) {
314
- _tgt._hooks.unmount[0]?.();
315
- _tgt._hooks.unmount = [];
316
- }
317
264
  return patchValue(parent, oldNode, _prevChild, childNew, ctx);
318
265
  }
319
266
  if (newV.type === Fragment) {
@@ -322,6 +269,12 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
322
269
  }
323
270
  if (typeof newV.type === "string") {
324
271
  if (oldNode && oldNode.nodeType === 1) {
272
+ const oldRef = oldV.props?.ref;
273
+ const newRef = newV.props?.ref;
274
+ if (oldRef !== newRef) {
275
+ if (typeof oldRef === "function") oldRef(null);
276
+ if (typeof newRef === "function") newRef(oldNode);
277
+ }
325
278
  patchProps(oldNode, oldV.props, newV.props);
326
279
  patchChildren(oldNode, oldV, newV, ctx);
327
280
  } else if (oldNode) {
@@ -358,6 +311,7 @@ function patchProps(el, oldProps, newProps) {
358
311
  const newKeys = newProps ? Object.keys(newProps).filter((k) => k !== "children" && k !== "key" && k !== "innerHTML") : [];
359
312
  for (const key of oldKeys) {
360
313
  if (!newKeys.includes(key)) {
314
+ if (key === "ref") continue;
361
315
  if (key.startsWith("on") && typeof oldProps[key] === "function") {
362
316
  el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
363
317
  } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
@@ -369,6 +323,7 @@ function patchProps(el, oldProps, newProps) {
369
323
  }
370
324
  }
371
325
  for (const key of newKeys) {
326
+ if (key === "ref") continue;
372
327
  const oldVal = oldProps?.[key];
373
328
  const newVal = newProps?.[key];
374
329
  if (key === "innerHTML") {
@@ -442,13 +397,17 @@ function patchSimpleChildren(parent, oldChildren, newChildren, ctx) {
442
397
  }
443
398
  }
444
399
  const max = Math.max(oldChildren.length, newChildren.length);
400
+ const oldNodes = [];
401
+ for (let i = 0; i < max; i++) {
402
+ oldNodes.push(parent.childNodes[i] || null);
403
+ }
445
404
  for (let i = 0; i < max; i++) {
446
405
  const oldChild = oldChildren[i];
447
406
  const newChild = newChildren[i];
448
- const existingNode = parent.childNodes[i] || null;
407
+ const existingNode = oldNodes[i];
449
408
  if (oldChild === void 0 && newChild !== void 0) {
450
409
  const node = renderValue(newChild, ctx);
451
- parent.appendChild(node);
410
+ parent.insertBefore(node, oldNodes[i + 1] ?? null);
452
411
  } else if (oldChild !== void 0 && newChild !== void 0) {
453
412
  patchValue(parent, existingNode, oldChild, newChild, ctx);
454
413
  }
@@ -497,12 +456,6 @@ function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
497
456
  }
498
457
  }
499
458
  }
500
- function runRefCleanup(vnode) {
501
- if (vnode._cleanup) {
502
- vnode._cleanup();
503
- vnode._cleanup = void 0;
504
- }
505
- }
506
459
  function cleanupPortalChildren(vnode) {
507
460
  const child = vnode._child;
508
461
  if (child == null) return;
@@ -533,16 +486,12 @@ function callRefCleanup(input) {
533
486
  if (child && typeof child === "object") callRefCleanup(child);
534
487
  }
535
488
  }
536
- if (vnode._$ && vnode._$._hooks?.unmount) {
537
- vnode._$._hooks.unmount[0]?.();
538
- vnode._$._hooks.unmount = [];
539
- }
489
+ if (typeof vnode.props?.ref === "function") vnode.props.ref(null);
540
490
  if (vnode._portalEl) {
541
491
  cleanupPortalChildren(vnode);
542
492
  vnode._portalEl.remove();
543
493
  vnode._portalEl = void 0;
544
494
  }
545
- if (vnode._cleanup) runRefCleanup(vnode);
546
495
  }
547
496
 
548
497
  // src/client/router.ts
@@ -737,18 +686,35 @@ function wrapComponent(Comp, _ctx) {
737
686
  return { type: Comp, props: {}, key: void 0 };
738
687
  }
739
688
  function createReactiveState(dirty) {
740
- return new Proxy({}, {
741
- set(target, key, value) {
742
- const old = Reflect.get(target, key);
743
- if (old === value) return true;
744
- Reflect.set(target, key, value);
745
- dirty();
746
- return true;
747
- },
748
- get(target, key) {
749
- return Reflect.get(target, key);
750
- }
751
- });
689
+ const proxyCache = /* @__PURE__ */ new WeakMap();
690
+ const reactive = (target) => {
691
+ if (target === null || typeof target !== "object") return target;
692
+ if (proxyCache.has(target)) return proxyCache.get(target);
693
+ const proxy = new Proxy(target, {
694
+ set(target2, key, value) {
695
+ const old = Reflect.get(target2, key);
696
+ if (old === value) return true;
697
+ Reflect.set(target2, key, value);
698
+ dirty();
699
+ return true;
700
+ },
701
+ get(target2, key) {
702
+ const value = Reflect.get(target2, key);
703
+ if (typeof value === "object" && value !== null) return reactive(value);
704
+ return value;
705
+ },
706
+ deleteProperty(target2, key) {
707
+ if (Reflect.has(target2, key)) {
708
+ Reflect.deleteProperty(target2, key);
709
+ dirty();
710
+ }
711
+ return true;
712
+ }
713
+ });
714
+ proxyCache.set(target, proxy);
715
+ return proxy;
716
+ };
717
+ return reactive({});
752
718
  }
753
719
 
754
720
  // src/client/types.ts
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 支持:
8
8
  * - key 属性(keyed diff)
9
- * - ctx.ui.onmount/onmounted/onunmount/onupdate 生命周期
9
+ * - ref / keyed diff
10
10
  *
11
11
  * 状态管理:组件使用闭包变量 + ctx.ui.render() 手动触发重渲染。
12
12
  */
@@ -12,14 +12,6 @@ export interface WfuiContext {
12
12
  dirty: () => void;
13
13
  /** 创建响应式状态容器:$.x = val 自动触发 dirty()(微任务批量渲染) */
14
14
  $: () => Record<string, any>;
15
- /** 组件首次渲染后触发(DOM 未创建) */
16
- onmount: (fn: () => void) => void;
17
- /** 组件 DOM 创建后触发,接收根元素,返回值作为 cleanup */
18
- onmounted: (fn: (el: Element) => (() => void) | void) => void;
19
- /** 组件移除前清理 */
20
- onunmount: (fn: () => void) => void;
21
- /** props 变化时触发,接收旧 props */
22
- onupdate: (fn: (prevProps: any) => void) => void;
23
15
  };
24
16
  /** 路由(由 router 中间件注入) */
25
17
  route?: {
@@ -13,11 +13,8 @@ export interface VNode {
13
13
  props: Record<string, any>;
14
14
  key?: string;
15
15
  el?: Node;
16
- _$?: Record<string, any>;
17
16
  /** 子 VNode 缓存(用于 patchValue diff,避免重复执行组件) */
18
17
  _child?: any;
19
- /** ref 回调返回的清理函数,卸载时由框架调用 */
20
- _cleanup?: (() => void) | undefined;
21
18
  /** Portal 子容器 DOM */
22
19
  _portalEl?: HTMLDivElement | undefined;
23
20
  /** 两阶段组件的 render 函数(mount 返回的函数) */
@@ -1,6 +1,22 @@
1
1
  import type { Component } from '../../client/vnode.ts';
2
+ export interface ValidationRule {
3
+ required?: boolean;
4
+ pattern?: RegExp;
5
+ minLength?: number;
6
+ maxLength?: number;
7
+ message: string;
8
+ /** 自定义验证函数,返回 true = 通过,false/string = 失败(string 会覆盖 message) */
9
+ validator?: (value: any) => boolean | string | Promise<boolean | string>;
10
+ }
2
11
  export interface FormProps {
3
- onSubmit?: () => void | Promise<void>;
12
+ /** 提交回调,接收字段名→值的对象 */
13
+ onSubmit?: (values: Record<string, any>) => void | Promise<void>;
14
+ /** 验证规则:字段名 → 规则数组 */
15
+ validation?: Record<string, ValidationRule[]>;
16
+ /** 验证失败时回调,接收字段名→错误消息的对象 */
17
+ onError?: (errors: Record<string, string>) => void;
4
18
  children?: any;
5
19
  }
20
+ /** 验证函数:字段值 → 验证规则 → 错误消息 */
21
+ export declare function validateValues(values: Record<string, any>, rules: Record<string, ValidationRule[]>): Promise<Record<string, string>>;
6
22
  export declare const Form: Component<FormProps>;
@@ -8,5 +8,9 @@ export interface ModalProps {
8
8
  onClose?: () => void;
9
9
  children?: any;
10
10
  footer?: any;
11
+ /** 自定义宽度,如 '500px'、'80%',默认 400px */
12
+ width?: string;
13
+ /** 是否显示关闭按钮,默认 true */
14
+ closable?: boolean;
11
15
  }
12
16
  export declare const Modal: Component<ModalProps>;
@@ -12,7 +12,11 @@ export interface SelectProps {
12
12
  required?: boolean;
13
13
  disabled?: boolean;
14
14
  error?: string;
15
- onChange?: (e: Event) => void;
15
+ onChange?: (value: string) => void;
16
16
  children?: any;
17
+ /** 启用搜索过滤 */
18
+ searchable?: boolean;
19
+ /** 异步搜索回调,返回值作为新选项列表 */
20
+ onSearch?: (keyword: string) => SelectOption[] | Promise<SelectOption[]>;
17
21
  }
18
22
  export declare const Select: Component<SelectProps>;
@@ -2,7 +2,7 @@
2
2
  * weifuwu/components — Skeleton
3
3
  */
4
4
  import type { Component } from '../../client/vnode.ts';
5
- export type SkeletonVariant = 'text' | 'circle' | 'rect';
5
+ export type SkeletonVariant = 'text' | 'circle' | 'rect' | 'image' | 'avatar' | 'table';
6
6
  export interface SkeletonProps {
7
7
  variant?: SkeletonVariant;
8
8
  lines?: number;
@@ -3,11 +3,24 @@ export interface TableColumn {
3
3
  key: string;
4
4
  label: string;
5
5
  width?: number | string;
6
+ /** 是否可排序 */
7
+ sortable?: boolean;
8
+ /** 自定义排序函数,默认按字符串比较 */
9
+ sorter?: (a: any, b: any) => number;
10
+ /** 自定义渲染 */
6
11
  render?: (value: any, row: any, index: number) => any;
7
12
  }
8
13
  export interface TableProps {
9
14
  data?: any[];
10
15
  columns: TableColumn[];
11
16
  onRowClick?: (row: any, index: number) => void;
17
+ /** 当前排序列的 key */
18
+ sortKey?: string;
19
+ /** 当前排序方向 */
20
+ sortOrder?: 'asc' | 'desc';
21
+ /** 排序变化回调 */
22
+ onSort?: (key: string, order: 'asc' | 'desc') => void;
23
+ /** 数据为空时显示的文本 */
24
+ emptyText?: string;
12
25
  }
13
26
  export declare const Table: Component<TableProps>;
@@ -1,12 +1,21 @@
1
1
  import type { Component } from '../../client/vnode.ts';
2
2
  export type ToastType = 'success' | 'error' | 'info' | 'warning';
3
+ export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center';
3
4
  export interface ToastItem {
4
5
  id: string;
5
6
  type: ToastType;
6
7
  message: string;
8
+ /** 此条目的自动消失时间(ms),覆盖 Toast 的默认 duration */
9
+ duration?: number;
7
10
  }
8
11
  export interface ToastProps {
9
12
  toasts?: ToastItem[];
10
13
  onRemove?: (id: string) => void;
14
+ /** 容器位置,默认 top-right */
15
+ position?: ToastPosition;
16
+ /** 全局默认自动消失时间(ms),0 = 不自动消失,默认 0 */
17
+ duration?: number;
18
+ /** 最大显示条数,超出时移除最早条目,默认 0 = 不限制 */
19
+ max?: number;
11
20
  }
12
21
  export declare const Toast: Component<ToastProps>;
@@ -50,7 +50,7 @@ export type { StatCardProps } from './StatCard/StatCard.ts';
50
50
  export { Steps } from './Steps/Steps.ts';
51
51
  export type { StepsProps, StepItem } from './Steps/Steps.ts';
52
52
  export { Form } from './Form/Form.ts';
53
- export type { FormProps } from './Form/Form.ts';
53
+ export type { FormProps, ValidationRule } from './Form/Form.ts';
54
54
  export { Field } from './Field/Field.ts';
55
55
  export type { FieldProps } from './Field/Field.ts';
56
56
  export { Slider } from './Slider/Slider.ts';