@wwog/react 1.2.20 → 1.3.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.
- package/README.md +16 -8
- package/dist/index.d.mts +60 -46
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/Sundry/index.ts +0 -1
- package/src/utils/createExternalState.test.tsx +39 -2
- package/src/utils/createExternalState.ts +87 -22
- /package/src/components/Sundry/{Clamp.tsx → Clamp.bak} +0 -0
package/README.md
CHANGED
|
@@ -189,7 +189,9 @@ function UserList({ users }) {
|
|
|
189
189
|
|
|
190
190
|
#### `<Clamp>` (v1.2.14+)
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
> Removed in v1.3.0. The compatibility problem is too big, the desktop web page works well, h5 has a problem.
|
|
193
|
+
|
|
194
|
+
A component for displaying text with a fixed number of lines, ellipsis, and optional extra content.
|
|
193
195
|
|
|
194
196
|
```tsx
|
|
195
197
|
import { Clamp } from "@wwog/react";
|
|
@@ -393,14 +395,19 @@ You can also use a container wrapper element:
|
|
|
393
395
|
|
|
394
396
|
#### `createExternalState` (v1.2.9+, useGetter added in v1.2.13)
|
|
395
397
|
|
|
396
|
-
|
|
398
|
+
> v1.2.21: Refactor the API to move sideeffects into options and enhance support for the transform interface
|
|
399
|
+
> v1.2.13: add useGetter
|
|
400
|
+
|
|
401
|
+
> A lightweight external state management utility that allows you to create and manage state outside the React component tree while maintaining perfect integration with components.
|
|
397
402
|
|
|
398
403
|
```tsx
|
|
399
404
|
import { createExternalState } from "@wwog/react";
|
|
400
405
|
|
|
401
406
|
// Create a global theme state
|
|
402
|
-
const themeState = createExternalState("light",
|
|
403
|
-
|
|
407
|
+
const themeState = createExternalState("light", {
|
|
408
|
+
sideEffect: (newTheme, oldTheme) => {
|
|
409
|
+
console.log(`Theme changed from ${oldTheme} to ${newTheme}`);
|
|
410
|
+
},
|
|
404
411
|
});
|
|
405
412
|
|
|
406
413
|
// Get or modify state from anywhere
|
|
@@ -429,16 +436,17 @@ function ReadOnlyThemeConsumer() {
|
|
|
429
436
|
}
|
|
430
437
|
```
|
|
431
438
|
|
|
432
|
-
- `createExternalState<T>(initialState,
|
|
439
|
+
- `createExternalState<T>(initialState, options?)`: Creates a state accessible outside components
|
|
440
|
+
|
|
433
441
|
- `initialState`: Initial state value
|
|
434
|
-
- `sideEffect`: Optional side effect function, called on state updates
|
|
442
|
+
- `options.sideEffect`: Optional side effect function, called on state updates
|
|
435
443
|
- Returns an object with methods:
|
|
436
444
|
- `get()`: Get the current state value
|
|
437
445
|
- `set(newState)`: Update the state value
|
|
438
446
|
- `use()`: React Hook, returns `[state, setState]` for using this state in components
|
|
439
447
|
- `useGetter()`: React Hook that only returns the state value, useful when you only need to read the state
|
|
440
|
-
|
|
441
|
-
Use cases:
|
|
448
|
+
- `options.transform`: - `get` - `set`
|
|
449
|
+
Use cases:
|
|
442
450
|
|
|
443
451
|
- Global state management (themes, user settings, etc.)
|
|
444
452
|
- Cross-component communication
|
package/dist/index.d.mts
CHANGED
|
@@ -372,35 +372,6 @@ interface ToggleProps<T = boolean> {
|
|
|
372
372
|
*/
|
|
373
373
|
declare const Toggle: <T>(props: ToggleProps<T>) => React$1.ReactNode;
|
|
374
374
|
|
|
375
|
-
interface ClampProps {
|
|
376
|
-
/**
|
|
377
|
-
* @description 最大行数
|
|
378
|
-
* @description_en maximum number of lines
|
|
379
|
-
* @default 1
|
|
380
|
-
*/
|
|
381
|
-
maxLine?: number;
|
|
382
|
-
extraContent?: React$1.ReactNode;
|
|
383
|
-
/**
|
|
384
|
-
* @description 用于控制额外内容的高度,如果出现没有正常显示请调节此属性
|
|
385
|
-
* @description_en used to control the height of the extra content. If it does not display normally, please adjust this property
|
|
386
|
-
* @default 20
|
|
387
|
-
*/
|
|
388
|
-
extraHeight?: number;
|
|
389
|
-
/**
|
|
390
|
-
* @description 显示的文本
|
|
391
|
-
* @description_en text to be displayed
|
|
392
|
-
*/
|
|
393
|
-
text: string;
|
|
394
|
-
wrapperStyle?: React$1.CSSProperties;
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* @description 用于固定行数,显示省略号且显示额外内容的组件。兼容性非常好,没有用到webkit-box和js。
|
|
398
|
-
* @description_en used to fix the number of lines, display ellipsis and display extra content. The compatibility is very good, without using webkit-box and js.
|
|
399
|
-
* @param props
|
|
400
|
-
* @returns
|
|
401
|
-
*/
|
|
402
|
-
declare const Clamp: FC<ClampProps>;
|
|
403
|
-
|
|
404
375
|
interface ArrayRenderProps<T> {
|
|
405
376
|
items: T[];
|
|
406
377
|
renderItem: (item: T, index: number) => React$1.ReactNode;
|
|
@@ -492,49 +463,92 @@ type CreateStateListener<T> = (state: T) => void;
|
|
|
492
463
|
* @param prevState The previous state value / 之前的状态值
|
|
493
464
|
*/
|
|
494
465
|
type ExternalSideEffect<T> = (newState: T, prevState: T) => any | Promise<any>;
|
|
466
|
+
/**
|
|
467
|
+
* @en Transform functions for getting and setting state
|
|
468
|
+
* @zh 用于获取和设置状态的转换函数
|
|
469
|
+
* @template T The type of the state / 状态的类型
|
|
470
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
471
|
+
*/
|
|
472
|
+
interface Transform<T, U = T> {
|
|
473
|
+
/**
|
|
474
|
+
* @en Transform function for getting state
|
|
475
|
+
* @zh 获取状态时的转换函数
|
|
476
|
+
*/
|
|
477
|
+
get?: (state: T) => U;
|
|
478
|
+
/**
|
|
479
|
+
* @en Transform function for setting state
|
|
480
|
+
* @zh 设置状态时的转换函数
|
|
481
|
+
*/
|
|
482
|
+
set?: (value: U) => T;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* @en Options for creating external state
|
|
486
|
+
* @zh 创建外部状态的选项
|
|
487
|
+
* @template T The type of the state / 状态的类型
|
|
488
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
489
|
+
*/
|
|
490
|
+
interface ExternalStateOptions<T, U = T> {
|
|
491
|
+
/**
|
|
492
|
+
* @en Side effect function to run after state changes
|
|
493
|
+
* @zh 状态变更后运行的副作用函数
|
|
494
|
+
*/
|
|
495
|
+
sideEffect?: ExternalSideEffect<T>;
|
|
496
|
+
/**
|
|
497
|
+
* @en Transform functions for getting and setting state
|
|
498
|
+
* @zh 用于获取和设置状态的转换函数
|
|
499
|
+
*/
|
|
500
|
+
transform?: Transform<T, U>;
|
|
501
|
+
}
|
|
495
502
|
/**
|
|
496
503
|
* @en External state management interface
|
|
497
504
|
* @zh 外部状态管理接口
|
|
498
505
|
* @template T The type of the state / 状态的类型
|
|
506
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
499
507
|
*/
|
|
500
|
-
interface ExternalState<T> {
|
|
508
|
+
interface ExternalState<T, U = T> {
|
|
501
509
|
/**
|
|
502
510
|
* @en Get the current state value
|
|
503
511
|
* @zh 获取当前状态值
|
|
504
|
-
* @returns The current state value /
|
|
512
|
+
* @returns The current state value (transformed if transform.get is provided) / 当前状态值(如果提供了 transform.get 则进行转换)
|
|
505
513
|
*/
|
|
506
|
-
get: () =>
|
|
514
|
+
get: () => U;
|
|
507
515
|
/**
|
|
508
516
|
* @en Set a new state value
|
|
509
517
|
* @zh 设置新的状态值
|
|
510
|
-
* @param newState The new state value /
|
|
518
|
+
* @param newState The new state value or a function that returns it / 新的状态值或返回新状态的函数
|
|
511
519
|
*/
|
|
512
|
-
set: (newState:
|
|
520
|
+
set: (newState: U | ((prevState: U) => U)) => void;
|
|
513
521
|
/**
|
|
514
522
|
* @en React Hook for using external state in components
|
|
515
523
|
* @zh 在组件中使用外部状态的 React Hook
|
|
516
|
-
* @returns Array containing current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
524
|
+
* @returns Array containing current钣金龙8国际唯一官网 current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
517
525
|
*/
|
|
518
|
-
use: () => [
|
|
526
|
+
use: () => [U, (newState: U | ((prevState: U) => U)) => void];
|
|
519
527
|
/**
|
|
520
528
|
* @zh use的变体,只获取value.
|
|
521
529
|
* @en A variant of use that only gets the value.
|
|
522
530
|
*/
|
|
523
|
-
useGetter: () =>
|
|
531
|
+
useGetter: () => U;
|
|
524
532
|
}
|
|
525
|
-
interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
533
|
+
interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
|
|
526
534
|
__listeners: CreateStateListener<T>[];
|
|
527
535
|
}
|
|
528
536
|
/**
|
|
529
537
|
*
|
|
530
538
|
* @example
|
|
531
539
|
* ```tsx
|
|
532
|
-
* // Create an app-level theme state
|
|
533
|
-
* const themeState = createExternalState('light'
|
|
540
|
+
* // Create an app-level theme state with options
|
|
541
|
+
* const themeState = createExternalState('light', {
|
|
542
|
+
* sideEffect: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
|
|
543
|
+
* transform: {
|
|
544
|
+
* get: (state) => state.toUpperCase(),
|
|
545
|
+
* set: (value) => value.toLowerCase()
|
|
546
|
+
* }
|
|
547
|
+
* });
|
|
534
548
|
*
|
|
535
549
|
* // Get or modify state outside components
|
|
536
|
-
* console.log(themeState.get()); // '
|
|
537
|
-
* themeState.set('dark');
|
|
550
|
+
* console.log(themeState.get()); // 'LIGHT'
|
|
551
|
+
* themeState.set((prev) => prev === 'light' ? 'dark' : 'light'); // Toggle theme
|
|
538
552
|
*
|
|
539
553
|
* // Use state in components
|
|
540
554
|
* function ThemeConsumer() {
|
|
@@ -542,7 +556,7 @@ interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
542
556
|
*
|
|
543
557
|
* return (
|
|
544
558
|
* <div className={theme}>
|
|
545
|
-
* <button onClick={() => setTheme(
|
|
559
|
+
* <button onClick={() => setTheme((prev) => prev === 'LIGHT' ? 'DARK' : 'LIGHT')}>
|
|
546
560
|
* Toggle theme / 切换主题
|
|
547
561
|
* </button>
|
|
548
562
|
* </div>
|
|
@@ -550,7 +564,7 @@ interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
550
564
|
* }
|
|
551
565
|
* ```
|
|
552
566
|
*/
|
|
553
|
-
declare function createExternalState<T>(initialState: T | (() => T),
|
|
567
|
+
declare function createExternalState<T, U = T>(initialState: T | (() => T), options?: ExternalStateOptions<T, U>): ExternalState<T, U>;
|
|
554
568
|
|
|
555
569
|
/**
|
|
556
570
|
* @description 性能优化,替代 React.Children.forEach, 回调可以返回 false 来中断循环
|
|
@@ -599,5 +613,5 @@ declare class Counter {
|
|
|
599
613
|
|
|
600
614
|
declare const safePromiseTry: <T, U extends unknown[]>(callbackFn: (...args: U) => T | PromiseLike<T>, ...args: U) => Promise<Awaited<T>>;
|
|
601
615
|
|
|
602
|
-
export { ArrayRender,
|
|
603
|
-
export type { ArrayRenderProps,
|
|
616
|
+
export { ArrayRender, Counter, DateRender, False, If, Pipe, Scope, SizeBox, Styles, Switch, Toggle, True, When, childrenLoop, createExternalState, cx, formatDate, safePromiseTry, useControlled };
|
|
617
|
+
export type { ArrayRenderProps, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalStateOptions, ExternalWithKernel, FalseProps, IfProps, PipeProps, ScopeProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r,{useMemo as
|
|
1
|
+
import r,{useMemo as h,Children as J,Fragment as S,isValidElement as _,cloneElement as R,useEffect as W,useState as x,useCallback as H}from"react";function C(t,n){if(t===void 0)return;let e=0;if(Array.isArray(t)){for(const l of t)if(n(l,e++)===!1)break}else n(t,e)}const B=(t,n)=>t===n,E=t=>r.createElement(r.Fragment,null,t.children);E.displayName="Switch_Case";const w=t=>r.createElement(r.Fragment,null,t.children);w.displayName="Switch_Default";const y=t=>{const{value:n,compare:e=B,children:l,strict:o=!1}=t,a=new Set;let s=null,f=null,m=!1;return C(l,(i,c)=>{if(!r.isValidElement(i))throw new Error(`Switch Children only accepts valid React elements at index ${c}`);const u=i.type;if(u.displayName===E.displayName){const d=i.props;if(a.has(d.value))throw new Error(`Switch found duplicate Case value at index ${c}: ${JSON.stringify(d.value)}${o?" (detected in strict mode)":""}`);if(a.add(d.value),!s&&e(n,d.value)&&(s=d.children,o===!1))return!1}else if(u.displayName===w.displayName){if(m)throw new Error(`Switch can only have one Default child at index ${c}`);if(m=!0,f=i.props.children,!o&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(u.displayName||u.name||u)} at index ${c}`)}),r.createElement(r.Fragment,null,s??f)};y.displayName="Switch",y.Case=E,y.Default=w,y.createTyped=function(){return{Switch:y,Case:E,Default:w}};const N=t=>r.createElement(r.Fragment,null,t.children),v=({children:t})=>r.createElement(r.Fragment,null,t),F=t=>r.createElement(r.Fragment,null,t.children);N.displayName="If_Then",v.displayName="If_Else",F.displayName="If_ElseIf";const p=({condition:t,children:n})=>{let e=null,l=null;const o=[];if(r.Children.forEach(n,a=>{if(!r.isValidElement(a))throw new Error("If component only accepts valid React elements");const s=a.type;if(s.displayName===N.displayName){if(e)throw new Error("If component can only have one Then child");e=a}else if(s.displayName===F.displayName)o.push(a);else if(s.displayName===v.displayName){if(l)throw new Error("If component can only have one Else child");l=a}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),t)return e?r.createElement(r.Fragment,null,e.props.children):null;for(const a of o)if(a.props.condition)return r.createElement(r.Fragment,null,a.props.children);return l?r.createElement(r.Fragment,null,l.props.children):null};p.displayName="If",p.Then=N,p.ElseIf=F,p.Else=v,p.createTyped=function(){return{If:p,Then:N,ElseIf:F,Else:v}};const V=({condition:t,children:n})=>t?r.createElement(r.Fragment,null,n):null,Z=({condition:t,children:n})=>t===!1?r.createElement(r.Fragment,null,n):null,z=({all:t,any:n,none:e,children:l,fallback:o})=>h(()=>(t&&(n||e)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(t&&t.length>0&&t.every(Boolean)||n&&n.length>0&&n.some(Boolean)||e&&e.length>0&&e.every(a=>!a))),[t,n,e])?r.createElement(r.Fragment,null,l):r.createElement(r.Fragment,null,o||null),L=({data:t,transform:n,render:e,fallback:l})=>{const o=h(()=>n.reduce((a,s)=>s(a),t),[t,n]);return o==null?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(o))},G=t=>{const{children:n,h:e,w:l,size:o,height:a,width:s,className:f}=t;return r.createElement("div",{style:{width:o||l||s,height:o||e||a,flexShrink:0},className:f},n)},U=({let:t,props:n,children:e,fallback:l})=>{const o=h(()=>typeof t=="function"?t(n):t,[t,n]);return!e||!Object.keys(o).length?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(o))};function M(...t){const n=new Set;for(const e of t)if(e){if(typeof e=="string")n.add(e);else if(Array.isArray(e))e.forEach(l=>n.add(l));else if(typeof e=="object")for(const[l,o]of Object.entries(e))o&&n.add(l)}return Array.from(n).join(" ")}const q=t=>typeof t=="object"&&!!t,b=({className:t,children:n,asWrapper:e=!1})=>{if(!n)return null;if(J.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),r.createElement(S,null,n);if(!t)return r.createElement(S,null,n);const l=typeof t=="string"?t:M(...Object.values(t));if(e)return r.createElement(e===!0?"div":e,{className:l},n);if(_(n)){const o=n;let a=o?.props?.className;return o?.type?.displayName===b.displayName&&q(a)&&(a=M(...Object.values(a))),R(n,{className:M(l,a)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),r.createElement(S,null,n)};b.displayName="W/Styles";const K=t=>{const{index:n=0,options:e,next:l,render:o}=t;W(()=>{if(e.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${e.length}. Defaulting to first option.`)},[n,e]);const[a,s]=x(n),f=()=>{s(m=>e.length?l?l(m,e):(m+1)%e.length:m)};return o(e[a],f)};function Q(t){const{items:n,renderItem:e,filter:l}=t;return n?r.createElement(S,null,n.map((o,a)=>l&&!l(o)?null:e(o,a))):(console.error("ArrayRender: items is null"),null)}function X({source:t,format:n,children:e}){const l=h(()=>{if(t instanceof Date)return t;if(typeof t=="string"||typeof t=="number"){const a=new Date(t);return isNaN(a.getTime())?null:a}return null},[t]),o=h(()=>l?n?n(l):l.toLocaleString():null,[l,n]);return!o||!e?null:r.createElement(r.Fragment,null,e(o))}const $="onChange",ee="value";function te(t){const{defaultValue:n,onBeforeChange:e,trigger:l=$,valuePropName:o=ee,props:a}=t,s=Object.prototype.hasOwnProperty.call(a,o),[f,m]=x(n),i=s?a[o]:f,c=h(()=>a[l],[a,l]),u=H(d=>{const g=typeof d=="function"?d(i):d;e&&e(g,i)===!1||(s||m(g),c&&c(g))},[s,e,i,c]);return[i,u]}function ne(t,...n){try{const e=t(...n);return e instanceof Promise?e:Promise.resolve(e)}catch(e){return Promise.reject(e)}}const I=typeof Promise.try=="function"?Promise.try.bind(Promise):ne;function re(t,n={}){let e=typeof t=="function"?t():t;const l=[],{sideEffect:o,transform:a}=n,s=()=>{const i=e;return a?.get?a.get(i):i},f=i=>{const c=e,u=a?.get?a.get(c):c;e=a?.set?a.set(typeof i=="function"?i(u):i):typeof i=="function"?i(u):i,l.forEach(d=>d(e)),o&&I(o,e,c).catch(d=>{console.error("Error in external state side effect, Please do it within side effects:",d)})},m=()=>{const[i,c]=r.useState(e);return r.useEffect(()=>(l.push(c),()=>{const u=l.indexOf(c);u>-1&&l.splice(u,1)}),[]),[a?.get?a.get(i):i,f]};return{get:s,set:f,use:m,useGetter:()=>{const[i]=m();return i},__listeners:l}}function le(t,n){const e=n||new Date,l=e.getFullYear(),o=e.getMonth()+1,a=e.getDate(),s=e.getHours(),f=e.getMinutes(),m=e.getSeconds(),i=e.getMilliseconds(),c=e.getDay(),u=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],d=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],g=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],A=["January","February","March","April","May","June","July","August","September","October","November","December"],P=d[c],D=u[c],T=o-1,Y=A[T],k=g[T],O={YY:l.toString().slice(2),YYYY:l.toString(),M:o.toString(),MM:o.toString().padStart(2,"0"),MMM:k,MMMM:Y,D:a.toString(),DD:a.toString().padStart(2,"0"),d:c.toString(),dd:D,ddd:D,dddd:P,H:s.toString(),HH:s.toString().padStart(2,"0"),h:(s%12).toString(),hh:(s%12).toString().padStart(2,"0"),m:f.toString(),mm:f.toString().padStart(2,"0"),s:m.toString(),ss:m.toString().padStart(2,"0"),SSS:i.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return t.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,j=>O[j])}class ae{count=0;next(){return this.count++}}export{Q as ArrayRender,ae as Counter,X as DateRender,Z as False,p as If,L as Pipe,U as Scope,G as SizeBox,b as Styles,y as Switch,K as Toggle,V as True,z as When,C as childrenLoop,re as createExternalState,M as cx,le as formatDate,I as safePromiseTry,te as useControlled};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwog/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A practical React component library providing declarative flow control and common UI utility components to make your React code more concise and readable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -123,7 +123,9 @@ describe("createExternalState", () => {
|
|
|
123
123
|
it("测试副作用函数", () => {
|
|
124
124
|
const mockSideEffect = vi.fn((...args) => void 0);
|
|
125
125
|
const initialState: string = "initial";
|
|
126
|
-
const state = createExternalState(initialState,
|
|
126
|
+
const state = createExternalState(initialState, {
|
|
127
|
+
sideEffect: mockSideEffect,
|
|
128
|
+
});
|
|
127
129
|
state.set("updated");
|
|
128
130
|
expect(mockSideEffect).toHaveBeenCalledTimes(1);
|
|
129
131
|
expect(mockSideEffect).toHaveBeenCalledWith("updated", initialState);
|
|
@@ -135,7 +137,9 @@ describe("createExternalState", () => {
|
|
|
135
137
|
it("测试异步副作用函数", async () => {
|
|
136
138
|
const mockAsyncSideEffect = vi.fn().mockResolvedValue(undefined);
|
|
137
139
|
const initialState: string = "initial";
|
|
138
|
-
const state = createExternalState(initialState,
|
|
140
|
+
const state = createExternalState(initialState, {
|
|
141
|
+
sideEffect: mockAsyncSideEffect,
|
|
142
|
+
});
|
|
139
143
|
|
|
140
144
|
state.set("updated");
|
|
141
145
|
expect(mockAsyncSideEffect).toHaveBeenCalledTimes(1);
|
|
@@ -181,4 +185,37 @@ describe("createExternalState", () => {
|
|
|
181
185
|
expect(ageLocator.element().textContent).toBe("35");
|
|
182
186
|
expect(state.get()).toEqual({ name: "王五", age: 35 });
|
|
183
187
|
});
|
|
188
|
+
|
|
189
|
+
it("测试transform转换功能", async () => {
|
|
190
|
+
const state = createExternalState("hello", {
|
|
191
|
+
transform: {
|
|
192
|
+
get: (str) => str.toUpperCase(),
|
|
193
|
+
set: (str) => str.toLowerCase(),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(state.get()).toBe("HELLO");
|
|
198
|
+
|
|
199
|
+
state.set("WORLD");
|
|
200
|
+
expect(state.get()).toBe("WORLD");
|
|
201
|
+
|
|
202
|
+
function TestComponent() {
|
|
203
|
+
const [value, setValue] = state.use();
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
<span data-testid="value">{value}</span>
|
|
207
|
+
<button data-testid="update" onClick={() => setValue("TEST")}>
|
|
208
|
+
Update
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const { getByTestId } = render(<TestComponent />);
|
|
215
|
+
expect(getByTestId("value").element().textContent).toBe("WORLD");
|
|
216
|
+
|
|
217
|
+
await getByTestId("update").click();
|
|
218
|
+
expect(getByTestId("value").element().textContent).toBe("TEST");
|
|
219
|
+
expect(state.get()).toBe("TEST");
|
|
220
|
+
});
|
|
184
221
|
});
|
|
@@ -20,41 +20,80 @@ export type ExternalSideEffect<T> = (
|
|
|
20
20
|
prevState: T
|
|
21
21
|
) => any | Promise<any>;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @en Transform functions for getting and setting state
|
|
25
|
+
* @zh 用于获取和设置状态的转换函数
|
|
26
|
+
* @template T The type of the state / 状态的类型
|
|
27
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
28
|
+
*/
|
|
29
|
+
export interface Transform<T, U = T> {
|
|
30
|
+
/**
|
|
31
|
+
* @en Transform function for getting state
|
|
32
|
+
* @zh 获取状态时的转换函数
|
|
33
|
+
*/
|
|
34
|
+
get?: (state: T) => U;
|
|
35
|
+
/**
|
|
36
|
+
* @en Transform function for setting state
|
|
37
|
+
* @zh 设置状态时的转换函数
|
|
38
|
+
*/
|
|
39
|
+
set?: (value: U) => T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @en Options for creating external state
|
|
44
|
+
* @zh 创建外部状态的选项
|
|
45
|
+
* @template T The type of the state / 状态的类型
|
|
46
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
47
|
+
*/
|
|
48
|
+
export interface ExternalStateOptions<T, U = T> {
|
|
49
|
+
/**
|
|
50
|
+
* @en Side effect function to run after state changes
|
|
51
|
+
* @zh 状态变更后运行的副作用函数
|
|
52
|
+
*/
|
|
53
|
+
sideEffect?: ExternalSideEffect<T>;
|
|
54
|
+
/**
|
|
55
|
+
* @en Transform functions for getting and setting state
|
|
56
|
+
* @zh 用于获取和设置状态的转换函数
|
|
57
|
+
*/
|
|
58
|
+
transform?: Transform<T, U>;
|
|
59
|
+
}
|
|
60
|
+
|
|
23
61
|
/**
|
|
24
62
|
* @en External state management interface
|
|
25
63
|
* @zh 外部状态管理接口
|
|
26
64
|
* @template T The type of the state / 状态的类型
|
|
65
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
27
66
|
*/
|
|
28
|
-
export interface ExternalState<T> {
|
|
67
|
+
export interface ExternalState<T, U = T> {
|
|
29
68
|
/**
|
|
30
69
|
* @en Get the current state value
|
|
31
70
|
* @zh 获取当前状态值
|
|
32
|
-
* @returns The current state value /
|
|
71
|
+
* @returns The current state value (transformed if transform.get is provided) / 当前状态值(如果提供了 transform.get 则进行转换)
|
|
33
72
|
*/
|
|
34
|
-
get: () =>
|
|
73
|
+
get: () => U;
|
|
35
74
|
|
|
36
75
|
/**
|
|
37
76
|
* @en Set a new state value
|
|
38
77
|
* @zh 设置新的状态值
|
|
39
|
-
* @param newState The new state value /
|
|
78
|
+
* @param newState The new state value or a function that returns it / 新的状态值或返回新状态的函数
|
|
40
79
|
*/
|
|
41
|
-
set: (newState:
|
|
80
|
+
set: (newState: U | ((prevState: U) => U)) => void;
|
|
42
81
|
|
|
43
82
|
/**
|
|
44
83
|
* @en React Hook for using external state in components
|
|
45
84
|
* @zh 在组件中使用外部状态的 React Hook
|
|
46
|
-
* @returns Array containing current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
85
|
+
* @returns Array containing current钣金龙8国际唯一官网 current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
47
86
|
*/
|
|
48
|
-
use: () => [
|
|
87
|
+
use: () => [U, (newState: U | ((prevState: U) => U)) => void];
|
|
49
88
|
|
|
50
89
|
/**
|
|
51
90
|
* @zh use的变体,只获取value.
|
|
52
91
|
* @en A variant of use that only gets the value.
|
|
53
92
|
*/
|
|
54
|
-
useGetter: () =>
|
|
93
|
+
useGetter: () => U;
|
|
55
94
|
}
|
|
56
95
|
|
|
57
|
-
export interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
96
|
+
export interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
|
|
58
97
|
__listeners: CreateStateListener<T>[];
|
|
59
98
|
}
|
|
60
99
|
|
|
@@ -62,12 +101,18 @@ export interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
62
101
|
*
|
|
63
102
|
* @example
|
|
64
103
|
* ```tsx
|
|
65
|
-
* // Create an app-level theme state
|
|
66
|
-
* const themeState = createExternalState('light'
|
|
104
|
+
* // Create an app-level theme state with options
|
|
105
|
+
* const themeState = createExternalState('light', {
|
|
106
|
+
* sideEffect: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
|
|
107
|
+
* transform: {
|
|
108
|
+
* get: (state) => state.toUpperCase(),
|
|
109
|
+
* set: (value) => value.toLowerCase()
|
|
110
|
+
* }
|
|
111
|
+
* });
|
|
67
112
|
*
|
|
68
113
|
* // Get or modify state outside components
|
|
69
|
-
* console.log(themeState.get()); // '
|
|
70
|
-
* themeState.set('dark');
|
|
114
|
+
* console.log(themeState.get()); // 'LIGHT'
|
|
115
|
+
* themeState.set((prev) => prev === 'light' ? 'dark' : 'light'); // Toggle theme
|
|
71
116
|
*
|
|
72
117
|
* // Use state in components
|
|
73
118
|
* function ThemeConsumer() {
|
|
@@ -75,7 +120,7 @@ export interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
75
120
|
*
|
|
76
121
|
* return (
|
|
77
122
|
* <div className={theme}>
|
|
78
|
-
* <button onClick={() => setTheme(
|
|
123
|
+
* <button onClick={() => setTheme((prev) => prev === 'LIGHT' ? 'DARK' : 'LIGHT')}>
|
|
79
124
|
* Toggle theme / 切换主题
|
|
80
125
|
* </button>
|
|
81
126
|
* </div>
|
|
@@ -83,22 +128,39 @@ export interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
83
128
|
* }
|
|
84
129
|
* ```
|
|
85
130
|
*/
|
|
86
|
-
export function createExternalState<T>(
|
|
131
|
+
export function createExternalState<T, U = T>(
|
|
87
132
|
initialState: T | (() => T),
|
|
88
|
-
|
|
89
|
-
): ExternalState<T> {
|
|
133
|
+
options: ExternalStateOptions<T, U> = {}
|
|
134
|
+
): ExternalState<T, U> {
|
|
90
135
|
let state: T =
|
|
91
136
|
typeof initialState === "function"
|
|
92
137
|
? (initialState as () => T)()
|
|
93
138
|
: initialState;
|
|
94
139
|
|
|
95
140
|
const listeners: CreateStateListener<T>[] = [];
|
|
141
|
+
const { sideEffect, transform } = options;
|
|
96
142
|
|
|
97
|
-
const get = () =>
|
|
143
|
+
const get = () => {
|
|
144
|
+
const currentState = state;
|
|
145
|
+
return transform?.get
|
|
146
|
+
? transform.get(currentState)
|
|
147
|
+
: (currentState as unknown as U);
|
|
148
|
+
};
|
|
98
149
|
|
|
99
|
-
const set = (newState:
|
|
150
|
+
const set = (newState: U | ((prevState: U) => U)) => {
|
|
100
151
|
const prevState = state;
|
|
101
|
-
|
|
152
|
+
const transformedPrevState = transform?.get
|
|
153
|
+
? transform.get(prevState)
|
|
154
|
+
: (prevState as unknown as U);
|
|
155
|
+
state = transform?.set
|
|
156
|
+
? transform.set(
|
|
157
|
+
typeof newState === "function"
|
|
158
|
+
? (newState as (prev: U) => U)(transformedPrevState)
|
|
159
|
+
: newState
|
|
160
|
+
)
|
|
161
|
+
: ((typeof newState === "function"
|
|
162
|
+
? (newState as (prev: U) => U)(transformedPrevState)
|
|
163
|
+
: newState) as unknown as T);
|
|
102
164
|
|
|
103
165
|
listeners.forEach((listener) => listener(state));
|
|
104
166
|
if (sideEffect) {
|
|
@@ -112,7 +174,7 @@ export function createExternalState<T>(
|
|
|
112
174
|
};
|
|
113
175
|
|
|
114
176
|
const use = () => {
|
|
115
|
-
const [localState, setLocalState] = React.useState(state);
|
|
177
|
+
const [localState, setLocalState] = React.useState<T>(state);
|
|
116
178
|
|
|
117
179
|
React.useEffect(() => {
|
|
118
180
|
listeners.push(setLocalState);
|
|
@@ -124,7 +186,10 @@ export function createExternalState<T>(
|
|
|
124
186
|
};
|
|
125
187
|
}, []);
|
|
126
188
|
|
|
127
|
-
return [
|
|
189
|
+
return [
|
|
190
|
+
transform?.get ? transform.get(localState) : (localState as unknown as U),
|
|
191
|
+
set,
|
|
192
|
+
] as [U, (newState: U | ((prevState: U) => U)) => void];
|
|
128
193
|
};
|
|
129
194
|
|
|
130
195
|
const useGetter = () => {
|
|
File without changes
|