@cleanweb/oore 2.0.0-alpha.16 → 2.0.0-alpha.18
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/build/_cjs/base/index.d.ts +3 -0
- package/build/_cjs/base/index.js +19 -0
- package/build/_cjs/base/merged-state.d.ts +20 -0
- package/build/_cjs/base/merged-state.js +61 -0
- package/build/_cjs/base/methods.d.ts +58 -0
- package/build/_cjs/base/methods.js +95 -0
- package/build/_cjs/base/state/class-types.d.ts +20 -0
- package/build/_cjs/base/state/class-types.js +2 -0
- package/build/_cjs/base/state/class.d.ts +69 -0
- package/build/_cjs/base/state/class.js +129 -0
- package/build/_cjs/base/state/hook-types.d.ts +32 -0
- package/build/_cjs/base/state/hook-types.js +2 -0
- package/build/_cjs/base/state/hooks.d.ts +12 -0
- package/build/_cjs/base/state/hooks.js +41 -0
- package/build/_cjs/base/state/index.d.ts +9 -0
- package/build/_cjs/base/state/index.js +35 -0
- package/build/_cjs/classy/class/index.d.ts +128 -0
- package/build/_cjs/classy/class/index.js +174 -0
- package/build/_cjs/classy/class/types/extractor.d.ts +5 -0
- package/build/_cjs/classy/class/types/extractor.js +2 -0
- package/build/_cjs/classy/class/utils/function-name.d.ts +2 -0
- package/build/_cjs/classy/class/utils/function-name.js +17 -0
- package/build/_cjs/classy/index.d.ts +3 -0
- package/build/_cjs/classy/index.js +19 -0
- package/build/_cjs/classy/instance/index.d.ts +144 -0
- package/build/_cjs/classy/instance/index.js +177 -0
- package/build/_cjs/classy/instance/mount-callbacks.d.ts +5 -0
- package/build/_cjs/classy/instance/mount-callbacks.js +30 -0
- package/build/_cjs/classy/instance/types/hook.d.ts +13 -0
- package/build/_cjs/classy/instance/types/hook.js +2 -0
- package/build/_cjs/classy/logic/index.d.ts +116 -0
- package/build/_cjs/classy/logic/index.js +123 -0
- package/build/_cjs/classy/logic/types/hook.d.ts +16 -0
- package/build/_cjs/classy/logic/types/hook.js +2 -0
- package/build/_cjs/docs-src/api/base-classes.d.ts +3 -0
- package/build/_cjs/docs-src/api/base-classes.js +9 -0
- package/build/_cjs/docs-src/api/index.d.ts +13 -0
- package/build/_cjs/docs-src/api/index.js +44 -0
- package/build/_cjs/docs-src/api/references.d.ts +5 -0
- package/build/_cjs/docs-src/api/references.js +31 -0
- package/build/_cjs/helpers/errors.d.ts +10 -0
- package/build/_cjs/helpers/errors.js +21 -0
- package/build/_cjs/helpers/index.d.ts +13 -0
- package/build/_cjs/helpers/index.js +31 -0
- package/build/_cjs/helpers/mount-state.d.ts +5 -0
- package/build/_cjs/helpers/mount-state.js +25 -0
- package/build/_cjs/helpers/rerender.d.ts +24 -0
- package/build/_cjs/helpers/rerender.js +42 -0
- package/build/_cjs/helpers/type-guards.d.ts +1 -0
- package/build/_cjs/helpers/type-guards.js +8 -0
- package/build/_cjs/helpers/use-component/index.d.ts +6 -0
- package/build/_cjs/helpers/use-component/index.js +17 -0
- package/build/_cjs/helpers/use-component/types.d.ts +22 -0
- package/build/_cjs/helpers/use-component/types.js +2 -0
- package/build/_cjs/index.d.ts +3 -0
- package/build/_cjs/index.js +19 -0
- package/build/_cjs/slots/hook.d.ts +20 -0
- package/build/_cjs/slots/hook.js +143 -0
- package/build/_cjs/slots/index.d.ts +1 -0
- package/build/_cjs/slots/index.js +17 -0
- package/build/_cjs/slots/types.d.ts +131 -0
- package/build/_cjs/slots/types.js +2 -0
- package/build/base/index.js +19 -3
- package/build/base/merged-state.js +35 -54
- package/build/base/methods.js +23 -25
- package/build/base/state/class-types.js +2 -1
- package/build/base/state/class.js +49 -69
- package/build/base/state/hook-types.js +2 -1
- package/build/base/state/hooks.js +12 -12
- package/build/base/state/index.js +32 -4
- package/build/classy/class/index.js +89 -109
- package/build/classy/class/types/extractor.js +2 -1
- package/build/classy/class/utils/function-name.js +5 -1
- package/build/classy/index.js +19 -3
- package/build/classy/instance/index.js +62 -84
- package/build/classy/instance/mount-callbacks.js +12 -8
- package/build/classy/instance/types/hook.js +2 -1
- package/build/classy/logic/index.js +27 -28
- package/build/classy/logic/types/hook.js +2 -1
- package/build/docs-src/api/base-classes.js +9 -3
- package/build/docs-src/api/index.js +39 -8
- package/build/docs-src/api/references.js +31 -5
- package/build/globals.d.ts +130 -83
- package/build/helpers/errors.js +5 -1
- package/build/helpers/index.js +23 -5
- package/build/helpers/mount-state.js +10 -6
- package/build/helpers/rerender.js +16 -12
- package/build/helpers/type-guards.js +6 -2
- package/build/helpers/use-component/index.js +9 -5
- package/build/helpers/use-component/types.js +2 -1
- package/build/index.d.ts +0 -1
- package/build/index.js +19 -3
- package/build/slots/hook.js +58 -36
- package/build/slots/index.js +17 -1
- package/build/slots/types.js +2 -1
- package/build/tsconfig.json +5 -6
- package/package.json +31 -12
- package/build/globals.js +0 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./state"), exports);
|
|
18
|
+
__exportStar(require("./methods"), exports);
|
|
19
|
+
__exportStar(require("./merged-state"), exports);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import '../globals';
|
|
2
|
+
declare class MergedState<TState extends object> {
|
|
3
|
+
static useRefresh<TState extends object>(this: MergedState<TState>): void;
|
|
4
|
+
reservedKeys: string[];
|
|
5
|
+
valueKeys: string[];
|
|
6
|
+
private _initialValues_;
|
|
7
|
+
private _values_;
|
|
8
|
+
private setState;
|
|
9
|
+
private _setters_;
|
|
10
|
+
get put(): { [Key in keyof TState]: (value: TState[Key]) => void; };
|
|
11
|
+
get initialState(): TState;
|
|
12
|
+
constructor(initialState: TState);
|
|
13
|
+
putMany: (newValues: Partial<TState>) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Similar to {@link useCleanState},
|
|
17
|
+
* but uses a single `useState` call for the entire state object.
|
|
18
|
+
*/
|
|
19
|
+
export declare const useMergedState: <TState extends object>(initialState: TState) => MergedState<TState> & TState;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useMergedState = void 0;
|
|
4
|
+
require("../globals");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
class MergedState {
|
|
7
|
+
static useRefresh() {
|
|
8
|
+
[this._values_, this.setState] = (0, react_1.useState)(this.initialState);
|
|
9
|
+
}
|
|
10
|
+
get put() {
|
|
11
|
+
return Object.assign({}, this._setters_);
|
|
12
|
+
}
|
|
13
|
+
get initialState() {
|
|
14
|
+
return Object.assign({}, this._initialValues_);
|
|
15
|
+
}
|
|
16
|
+
constructor(initialState) {
|
|
17
|
+
this._initialValues_ = {};
|
|
18
|
+
this._values_ = {};
|
|
19
|
+
this._setters_ = {};
|
|
20
|
+
this.putMany = (newValues) => {
|
|
21
|
+
this.setState(Object.assign(Object.assign({}, this._values_), newValues));
|
|
22
|
+
};
|
|
23
|
+
this.reservedKeys = Object.keys(this);
|
|
24
|
+
this.valueKeys = [];
|
|
25
|
+
this._initialValues_ = Object.assign({}, initialState);
|
|
26
|
+
this._values_ = Object.assign({}, initialState);
|
|
27
|
+
Object.keys(initialState).forEach((_key) => {
|
|
28
|
+
const key = _key;
|
|
29
|
+
if (this.reservedKeys.includes(key)) {
|
|
30
|
+
throw new Error(`The name "${key}" is reserved by CleanState and cannot be used to index state variables. Please use a different key.`);
|
|
31
|
+
}
|
|
32
|
+
this.valueKeys.push(key);
|
|
33
|
+
this._setters_[key] = (value) => {
|
|
34
|
+
// this._values_[key] = value;
|
|
35
|
+
this.setState(Object.assign(Object.assign({}, this._values_), { [key]: value }));
|
|
36
|
+
};
|
|
37
|
+
const self = this;
|
|
38
|
+
Object.defineProperty(this, key, {
|
|
39
|
+
get() {
|
|
40
|
+
return self._values_[key];
|
|
41
|
+
},
|
|
42
|
+
set(value) {
|
|
43
|
+
self._setters_[key](value);
|
|
44
|
+
},
|
|
45
|
+
enumerable: true,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Similar to {@link useCleanState},
|
|
52
|
+
* but uses a single `useState` call for the entire state object.
|
|
53
|
+
*/
|
|
54
|
+
const useMergedState = (initialState) => {
|
|
55
|
+
const cleanState = (0, react_1.useRef)((0, react_1.useMemo)(() => {
|
|
56
|
+
return new MergedState(initialState);
|
|
57
|
+
}, [])).current;
|
|
58
|
+
MergedState.useRefresh.call(cleanState);
|
|
59
|
+
return cleanState;
|
|
60
|
+
};
|
|
61
|
+
exports.useMergedState = useMergedState;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module ComponentMethods
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @summary
|
|
6
|
+
* Base class for a class that holds methods for a function component.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* These methods will have access to the components state and props via
|
|
10
|
+
* `this.state` and `this.props` respectively.
|
|
11
|
+
*
|
|
12
|
+
* Call the {@link useMethods} hook inside your function component to instantiate the class.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ComponentMethods<TProps extends object = {}, TState extends object | null = null> {
|
|
15
|
+
readonly props: TProps;
|
|
16
|
+
readonly state: TState;
|
|
17
|
+
/**
|
|
18
|
+
* Persist class members during HMR. {@include ../classy/logic/hrm-preserve-keys.md}
|
|
19
|
+
* @privateRemarks
|
|
20
|
+
* @see {@link https://cleanjsweb.github.io/neat-react/classes/API.BaseClasses.ComponentMethods.html#_hmrpreservekeys | Full details}
|
|
21
|
+
*/
|
|
22
|
+
_hmrPreserveKeys: Array<keyof this | (string & {})>;
|
|
23
|
+
/**
|
|
24
|
+
* Run custom logic after HMR update. {@include ../classy/logic/on-hrm-update.md}
|
|
25
|
+
* @privateRemarks
|
|
26
|
+
* @see {@link https://cleanjsweb.github.io/neat-react/classes/API.BaseClasses.ComponentMethods.html#_onhmrupdate | Full details}
|
|
27
|
+
*/
|
|
28
|
+
_onHmrUpdate?: <TInstance extends this>(oldInstance: TInstance) => void;
|
|
29
|
+
}
|
|
30
|
+
type UseMethods = {
|
|
31
|
+
<Class extends typeof ComponentMethods<object, object>>(Methods: Class & Constructor<InstanceType<Class>>, props: InstanceType<Class>['props'], state: InstanceType<Class>['state']): InstanceType<Class>;
|
|
32
|
+
<Class extends typeof ComponentMethods<object, null>>(Methods: Class & Constructor<InstanceType<Class>>, props: InstanceType<Class>['props'], state?: null): InstanceType<Class>;
|
|
33
|
+
<Class extends typeof ComponentMethods<NeverObject, null>>(Methods: Class & Constructor<InstanceType<Class>>): InstanceType<Class>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Returns an instance of the provided class,
|
|
37
|
+
* with the state and props arguments added as instance members.
|
|
38
|
+
*
|
|
39
|
+
* `state` should be an instance of `CleanState` created with {@link useCleanState}.
|
|
40
|
+
*/
|
|
41
|
+
declare const useMethods: UseMethods;
|
|
42
|
+
export { useMethods };
|
|
43
|
+
/** /type_testing: {
|
|
44
|
+
let a = async () => {
|
|
45
|
+
const a: object = {b: ''};
|
|
46
|
+
|
|
47
|
+
type t = keyof typeof a;
|
|
48
|
+
|
|
49
|
+
class MyMethods extends ComponentMethods<EmptyObject, null> {
|
|
50
|
+
// static getInitialState = () => ({});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const { useCleanState } = (await import('./state.js'));
|
|
54
|
+
|
|
55
|
+
const self = useMethods(MyMethods, {});
|
|
56
|
+
self.state;
|
|
57
|
+
}
|
|
58
|
+
}/**/
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module ComponentMethods
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.useMethods = exports.ComponentMethods = void 0;
|
|
7
|
+
// Values
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
/**
|
|
10
|
+
* @summary
|
|
11
|
+
* Base class for a class that holds methods for a function component.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* These methods will have access to the components state and props via
|
|
15
|
+
* `this.state` and `this.props` respectively.
|
|
16
|
+
*
|
|
17
|
+
* Call the {@link useMethods} hook inside your function component to instantiate the class.
|
|
18
|
+
*/
|
|
19
|
+
class ComponentMethods {
|
|
20
|
+
constructor() {
|
|
21
|
+
/**
|
|
22
|
+
* Persist class members during HMR. {@include ../classy/logic/hrm-preserve-keys.md}
|
|
23
|
+
* @privateRemarks
|
|
24
|
+
* @see {@link https://cleanjsweb.github.io/neat-react/classes/API.BaseClasses.ComponentMethods.html#_hmrpreservekeys | Full details}
|
|
25
|
+
*/
|
|
26
|
+
this._hmrPreserveKeys = []; // @todo Keep undefined. Update to empty array after instantiation in dev env.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.ComponentMethods = ComponentMethods;
|
|
30
|
+
;
|
|
31
|
+
/**
|
|
32
|
+
* Returns an instance of the provided class,
|
|
33
|
+
* with the state and props arguments added as instance members.
|
|
34
|
+
*
|
|
35
|
+
* `state` should be an instance of `CleanState` created with {@link useCleanState}.
|
|
36
|
+
*/
|
|
37
|
+
const useMethods = (...args) => {
|
|
38
|
+
var _a;
|
|
39
|
+
const [Methods, props = {}, state] = args;
|
|
40
|
+
// Vite HMR seems to sometimes reinitialize useMemo calls after a hot update,
|
|
41
|
+
// causing the instance to be unexpectedly recreated in the middle of the component's lifecycle.
|
|
42
|
+
// But useRef and useState values appear to always be preserved whenever this happens.
|
|
43
|
+
// So those two are the only cross-render-persistence methods we can consider safe.
|
|
44
|
+
// In production, we only use the latestInstance the first time, and it's ignored every other time.
|
|
45
|
+
// This means changing the class at runtime will have no effect in production.
|
|
46
|
+
// latestInstance is only extracted into a separate variable for use in dev mode during HMR.
|
|
47
|
+
const latestInstance = (0, react_1.useMemo)(() => new Methods(), [Methods]);
|
|
48
|
+
const instanceRef = (0, react_1.useRef)(latestInstance);
|
|
49
|
+
const refreshState = () => {
|
|
50
|
+
// @ts-expect-error
|
|
51
|
+
instanceRef.current.props = props;
|
|
52
|
+
// @ts-expect-error
|
|
53
|
+
if (state)
|
|
54
|
+
instanceRef.current.state = state;
|
|
55
|
+
};
|
|
56
|
+
if (process.env.NODE_ENV === 'development' && instanceRef.current !== latestInstance) {
|
|
57
|
+
const oldInstance = instanceRef.current;
|
|
58
|
+
latestInstance._hmrPreserveKeys.forEach((_key) => {
|
|
59
|
+
const key = _key;
|
|
60
|
+
// @ts-expect-error We're assigning to readonly properties. Also, Typescript doesn't know that the type of the left and right side will always match, due to the dynamic access.
|
|
61
|
+
latestInstance[key] = oldInstance[key];
|
|
62
|
+
});
|
|
63
|
+
// Ensure that any stale references to oldInstance within the app
|
|
64
|
+
// will end up retrieving up-to-date values from latestInstance
|
|
65
|
+
// through the prototype chain.
|
|
66
|
+
Reflect.ownKeys(oldInstance).forEach((_key) => {
|
|
67
|
+
const key = _key;
|
|
68
|
+
delete oldInstance[key];
|
|
69
|
+
});
|
|
70
|
+
Object.setPrototypeOf(oldInstance, latestInstance);
|
|
71
|
+
instanceRef.current = latestInstance;
|
|
72
|
+
refreshState();
|
|
73
|
+
(_a = latestInstance._onHmrUpdate) === null || _a === void 0 ? void 0 : _a.call(latestInstance, oldInstance);
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
refreshState();
|
|
77
|
+
return instanceRef.current;
|
|
78
|
+
};
|
|
79
|
+
exports.useMethods = useMethods;
|
|
80
|
+
/** /type_testing: {
|
|
81
|
+
let a = async () => {
|
|
82
|
+
const a: object = {b: ''};
|
|
83
|
+
|
|
84
|
+
type t = keyof typeof a;
|
|
85
|
+
|
|
86
|
+
class MyMethods extends ComponentMethods<EmptyObject, null> {
|
|
87
|
+
// static getInitialState = () => ({});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const { useCleanState } = (await import('./state.js'));
|
|
91
|
+
|
|
92
|
+
const self = useMethods(MyMethods, {});
|
|
93
|
+
self.state;
|
|
94
|
+
}
|
|
95
|
+
}/**/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CleanStateBase } from './class';
|
|
2
|
+
import { TCleanState, TStateData } from './hook-types';
|
|
3
|
+
export type TCleanStateBase = typeof CleanStateBase;
|
|
4
|
+
export type TCleanStateBaseKeys = keyof TCleanStateBase;
|
|
5
|
+
export type PutState<TState extends TStateData> = {
|
|
6
|
+
[Key in keyof TState]: React.Dispatch<React.SetStateAction<TState[Key]>>;
|
|
7
|
+
};
|
|
8
|
+
export type PutManyPayload<TState extends TStateData> = {
|
|
9
|
+
[Key in keyof TState]?: React.SetStateAction<TState[Key]>;
|
|
10
|
+
};
|
|
11
|
+
export type StateFragment<TComponent extends {
|
|
12
|
+
getInitialState: (...args: any[]) => TStateData;
|
|
13
|
+
}> = PutManyPayload<ReturnType<TComponent['getInitialState']>>;
|
|
14
|
+
export type { StateFragment as SF };
|
|
15
|
+
export interface ICleanStateConstructor {
|
|
16
|
+
new <TState extends object>(...args: ConstructorParameters<typeof CleanStateBase>): TCleanState<TState>;
|
|
17
|
+
}
|
|
18
|
+
export type ICleanStateClass = ICleanStateConstructor & {
|
|
19
|
+
[Key in TCleanStateBaseKeys]: (typeof CleanStateBase)[Key];
|
|
20
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ICleanStateClass, ICleanStateConstructor, PutManyPayload, PutState } from './class-types';
|
|
2
|
+
/** @internal */
|
|
3
|
+
export declare class CleanStateBase<TState extends Record<string, any>> {
|
|
4
|
+
readonly reservedKeys: string[];
|
|
5
|
+
readonly valueKeys: string[];
|
|
6
|
+
private _values_;
|
|
7
|
+
private _initialValues_;
|
|
8
|
+
private _setters_;
|
|
9
|
+
constructor(initialState: TState);
|
|
10
|
+
static update: <TState_1 extends object>(this: CleanStateBase<TState_1>) => void;
|
|
11
|
+
get put(): PutState<TState>;
|
|
12
|
+
get initialState(): TState;
|
|
13
|
+
/**
|
|
14
|
+
* Accepts an object to be merged into the current state object,
|
|
15
|
+
* updating the values of specified keys to their specified values.
|
|
16
|
+
*
|
|
17
|
+
* All specified keys must be present on the initial state object.
|
|
18
|
+
* This is a requirement of React hooks, as the number of calls to
|
|
19
|
+
* useState must be the same throughout a component's lifetime.
|
|
20
|
+
*
|
|
21
|
+
* To dynamically add new keys to your state object, use a nested object,
|
|
22
|
+
* or use the {@link useMergedState | `useMergedState`} hook also exported by this library.
|
|
23
|
+
*
|
|
24
|
+
* When called from inside a component class, the type of the object passed
|
|
25
|
+
* must be explicitly specified with a type assertion.
|
|
26
|
+
* This is because the component classes use the dynamic `this` type to define
|
|
27
|
+
* the type of their state property.
|
|
28
|
+
*
|
|
29
|
+
* Use the `StateFragment` type, also aliased as `SF`, to define this type assertion.
|
|
30
|
+
* The assertion is not needed when calling putMany from outside the class.
|
|
31
|
+
*
|
|
32
|
+
* @example <caption>Using `putMany`</caption>
|
|
33
|
+
* import type { SF } from '@cleanweb/oore/base';
|
|
34
|
+
* import { ComponentLogic } from '@cleanweb/oore';
|
|
35
|
+
*
|
|
36
|
+
* class InputCL extends ComponentLogic<TProps> {
|
|
37
|
+
* getInitialState(props: TProps): TState => ({
|
|
38
|
+
* disabled: true,
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* enableInput = () => {
|
|
42
|
+
* this.state.putMany({
|
|
43
|
+
* disabled: false,
|
|
44
|
+
* } as SF<this>); // Type assertion required inside class.
|
|
45
|
+
* };
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* export const Input = (props: TProps) => {
|
|
49
|
+
* const self = useLogic(InputCL, props);
|
|
50
|
+
*
|
|
51
|
+
* return <>
|
|
52
|
+
* {
|
|
53
|
+
* // Other elements...
|
|
54
|
+
* }
|
|
55
|
+
* <button className="cta"
|
|
56
|
+
* onClick={() => {
|
|
57
|
+
* self.state.putMany({
|
|
58
|
+
* disabled: true
|
|
59
|
+
* }); // No type assertion needed outside the class.
|
|
60
|
+
* }}>
|
|
61
|
+
* Submit
|
|
62
|
+
* </button>
|
|
63
|
+
* </>;
|
|
64
|
+
* };
|
|
65
|
+
*/
|
|
66
|
+
readonly putMany: (newValues: PutManyPayload<TState>) => void;
|
|
67
|
+
}
|
|
68
|
+
/** @internal */
|
|
69
|
+
export declare const CleanState: (ICleanStateConstructor & ICleanStateClass);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CleanState = exports.CleanStateBase = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/** @internal */
|
|
6
|
+
class CleanStateBase {
|
|
7
|
+
constructor(initialState) {
|
|
8
|
+
this._values_ = {};
|
|
9
|
+
this._setters_ = {};
|
|
10
|
+
/**
|
|
11
|
+
* Accepts an object to be merged into the current state object,
|
|
12
|
+
* updating the values of specified keys to their specified values.
|
|
13
|
+
*
|
|
14
|
+
* All specified keys must be present on the initial state object.
|
|
15
|
+
* This is a requirement of React hooks, as the number of calls to
|
|
16
|
+
* useState must be the same throughout a component's lifetime.
|
|
17
|
+
*
|
|
18
|
+
* To dynamically add new keys to your state object, use a nested object,
|
|
19
|
+
* or use the {@link useMergedState | `useMergedState`} hook also exported by this library.
|
|
20
|
+
*
|
|
21
|
+
* When called from inside a component class, the type of the object passed
|
|
22
|
+
* must be explicitly specified with a type assertion.
|
|
23
|
+
* This is because the component classes use the dynamic `this` type to define
|
|
24
|
+
* the type of their state property.
|
|
25
|
+
*
|
|
26
|
+
* Use the `StateFragment` type, also aliased as `SF`, to define this type assertion.
|
|
27
|
+
* The assertion is not needed when calling putMany from outside the class.
|
|
28
|
+
*
|
|
29
|
+
* @example <caption>Using `putMany`</caption>
|
|
30
|
+
* import type { SF } from '@cleanweb/oore/base';
|
|
31
|
+
* import { ComponentLogic } from '@cleanweb/oore';
|
|
32
|
+
*
|
|
33
|
+
* class InputCL extends ComponentLogic<TProps> {
|
|
34
|
+
* getInitialState(props: TProps): TState => ({
|
|
35
|
+
* disabled: true,
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* enableInput = () => {
|
|
39
|
+
* this.state.putMany({
|
|
40
|
+
* disabled: false,
|
|
41
|
+
* } as SF<this>); // Type assertion required inside class.
|
|
42
|
+
* };
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* export const Input = (props: TProps) => {
|
|
46
|
+
* const self = useLogic(InputCL, props);
|
|
47
|
+
*
|
|
48
|
+
* return <>
|
|
49
|
+
* {
|
|
50
|
+
* // Other elements...
|
|
51
|
+
* }
|
|
52
|
+
* <button className="cta"
|
|
53
|
+
* onClick={() => {
|
|
54
|
+
* self.state.putMany({
|
|
55
|
+
* disabled: true
|
|
56
|
+
* }); // No type assertion needed outside the class.
|
|
57
|
+
* }}>
|
|
58
|
+
* Submit
|
|
59
|
+
* </button>
|
|
60
|
+
* </>;
|
|
61
|
+
* };
|
|
62
|
+
*/
|
|
63
|
+
this.putMany = (newValues) => {
|
|
64
|
+
Object.entries(newValues).forEach((entry) => {
|
|
65
|
+
const [key, value] = entry;
|
|
66
|
+
this.put[key](value);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
this.reservedKeys = Object.keys(this);
|
|
70
|
+
/**
|
|
71
|
+
* The keys from the initial state object.
|
|
72
|
+
* By capturing and storing the value once, we ensure that any potential changes to the object,
|
|
73
|
+
* or irregularities in the order of keys returned by Object.keys,
|
|
74
|
+
* will not affect the order of subsequent useState calls.
|
|
75
|
+
* Only keys provided on the initial call will be recognized,
|
|
76
|
+
* since CleanState is instantiated only once with useMemo,
|
|
77
|
+
* and they will always be processed in a consistent order during rerenders.
|
|
78
|
+
*/
|
|
79
|
+
this.valueKeys = Object.keys(initialState);
|
|
80
|
+
this._initialValues_ = Object.assign({}, initialState);
|
|
81
|
+
this.valueKeys.forEach((key) => {
|
|
82
|
+
if (this.reservedKeys.includes(key))
|
|
83
|
+
throw new Error(`The name "${key}" is reserved by CleanState and cannot be used to index state variables. Please use a different key.`);
|
|
84
|
+
const self = this;
|
|
85
|
+
Object.defineProperty(this, key, {
|
|
86
|
+
get() {
|
|
87
|
+
return self._values_[key];
|
|
88
|
+
},
|
|
89
|
+
set(value) {
|
|
90
|
+
self._setters_[key](value);
|
|
91
|
+
},
|
|
92
|
+
enumerable: true,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
get put() {
|
|
97
|
+
return Object.assign({}, this._setters_);
|
|
98
|
+
}
|
|
99
|
+
get initialState() {
|
|
100
|
+
return Object.assign({}, this._initialValues_);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.CleanStateBase = CleanStateBase;
|
|
104
|
+
CleanStateBase.update = function update() {
|
|
105
|
+
if (!(this instanceof exports.CleanState))
|
|
106
|
+
throw new Error('CleanState.update must be called with `this` value set to a CleanState instance. Did you forget to use `.call` or `.apply`? Example: CleanState.update.call(cleanState);');
|
|
107
|
+
/**
|
|
108
|
+
* Linters complain about the use of a React hook within a loop because:
|
|
109
|
+
* > By following this rule, you ensure that Hooks are called in the same order each time a component renders.
|
|
110
|
+
* > That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
|
|
111
|
+
* To resolve this, we're calling `useState` via an alias `retrieveState`.
|
|
112
|
+
* Bypassing this rule is safe here because `useCleanState` is a special case,
|
|
113
|
+
* and it guarantees that the same useState calls will be made on every render in the exact same order.
|
|
114
|
+
* Therefore, it is safe to silence the linters, and required for this implementation to work smoothly.
|
|
115
|
+
*/
|
|
116
|
+
const retrieveState = react_1.useState;
|
|
117
|
+
this.valueKeys.forEach((key) => {
|
|
118
|
+
// @todo Make state updates accessible immediately. Use state.staged to access the scheduled updates.
|
|
119
|
+
let setter;
|
|
120
|
+
[this._values_[key], setter] = retrieveState(this.initialState[key]);
|
|
121
|
+
this._setters_[key] = ((valueOrCallback) => {
|
|
122
|
+
// this._staged_[key] = value;
|
|
123
|
+
setter(valueOrCallback);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
;
|
|
128
|
+
/** @internal */
|
|
129
|
+
exports.CleanState = (CleanStateBase);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CleanStateBase } from './class';
|
|
2
|
+
/**
|
|
3
|
+
* Base type for an `initialState` object.
|
|
4
|
+
* It is a regular object type, with some reserved keys excluded.
|
|
5
|
+
*/
|
|
6
|
+
export type TStateData = object & {
|
|
7
|
+
[Key in keyof CleanStateBase<{}>]?: never;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Describes a `CleanState` object instantiated with an `initialState`
|
|
11
|
+
* object of type `TState`.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam TState - The type of your `initialState` object.
|
|
14
|
+
*/
|
|
15
|
+
export type TCleanState<TState extends TStateData> = (CleanStateBase<TState> & Omit<TState, keyof CleanStateBase<{}>>);
|
|
16
|
+
/**
|
|
17
|
+
* Takes a `TCleanState` type and returns the `initialState` type
|
|
18
|
+
* associated with the provided `TCleanState`.
|
|
19
|
+
*
|
|
20
|
+
* This is useful to isolate the type of your actual state data without
|
|
21
|
+
* any of the reserved keys provided by the Clean State utility.
|
|
22
|
+
*/
|
|
23
|
+
export type ExtractCleanStateData<YourCleanState extends CleanStateBase<{}>> = Omit<YourCleanState, keyof CleanStateBase<{}>>;
|
|
24
|
+
type StateInitFunction = (...args: any[]) => object;
|
|
25
|
+
type StateInit = object | StateInitFunction;
|
|
26
|
+
export type TInitialState<Initializer extends StateInit> = Initializer extends (...args: any[]) => (infer TState extends object) ? TState : Initializer;
|
|
27
|
+
/**
|
|
28
|
+
* @typeParam TInit - An initial state object, or a function that
|
|
29
|
+
* returns the initial state object.
|
|
30
|
+
*/
|
|
31
|
+
export type TUseCleanState = <TInit extends StateInit>(_initialState: TInit, ...props: TInit extends (...args: infer TProps extends any[]) => (infer TState extends object) ? TProps : []) => TCleanState<TInitialState<TInit>>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TUseCleanState } from './hook-types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a state object, which includes the provided values,
|
|
4
|
+
* as well as helper methods for updating those values and automatically
|
|
5
|
+
* rerendering your component's UI to reflect the updates.
|
|
6
|
+
*
|
|
7
|
+
* Uses {@link React.useState} under the hood, with a separate call
|
|
8
|
+
* to `useState` for each top-level key in the provided object.
|
|
9
|
+
*
|
|
10
|
+
* Discussion: [When to `useCleanState`](https://cleanjsweb.github.io/neat-react/documents/Clean_State.html).
|
|
11
|
+
*/
|
|
12
|
+
export declare const useCleanState: TUseCleanState;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useCleanState = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const class_1 = require("./class");
|
|
6
|
+
/**
|
|
7
|
+
* Creates a state object, which includes the provided values,
|
|
8
|
+
* as well as helper methods for updating those values and automatically
|
|
9
|
+
* rerendering your component's UI to reflect the updates.
|
|
10
|
+
*
|
|
11
|
+
* Uses {@link React.useState} under the hood, with a separate call
|
|
12
|
+
* to `useState` for each top-level key in the provided object.
|
|
13
|
+
*
|
|
14
|
+
* Discussion: [When to `useCleanState`](https://cleanjsweb.github.io/neat-react/documents/Clean_State.html).
|
|
15
|
+
*/
|
|
16
|
+
const useCleanState = (_initialState, ...props) => {
|
|
17
|
+
const initialState = typeof _initialState === 'function'
|
|
18
|
+
? (0, react_1.useMemo)(() => _initialState(...props), [])
|
|
19
|
+
: _initialState;
|
|
20
|
+
;
|
|
21
|
+
const cleanState = (0, react_1.useRef)((0, react_1.useMemo)(() => {
|
|
22
|
+
return new class_1.CleanState(initialState);
|
|
23
|
+
}, [])).current;
|
|
24
|
+
class_1.CleanState.update.call(cleanState);
|
|
25
|
+
return cleanState;
|
|
26
|
+
};
|
|
27
|
+
exports.useCleanState = useCleanState;
|
|
28
|
+
// Should be valid.
|
|
29
|
+
// useCleanState((a: number) => ({b: a.toString(), q: 1}), 6);
|
|
30
|
+
// useCleanState((a: boolean) => ({b: a.toString()}), true);
|
|
31
|
+
// useCleanState((a: number, c?: string) => ({ b: `${a}` }), 6);
|
|
32
|
+
// useCleanState((a: number, c?: string) => ({ b: `${a}` }), 6, 'word');
|
|
33
|
+
// useCleanState((a: number, c: string) => ({ b: a + c, f: true }), 6, 'text');
|
|
34
|
+
// useCleanState({ d: 5000 });
|
|
35
|
+
// Should fail.
|
|
36
|
+
// useCleanState((a: number) => ({b: a.toString(), q: 1}), 6, false);
|
|
37
|
+
// useCleanState((a: boolean) => ({b: a.toString()}));
|
|
38
|
+
// useCleanState((a: number, c?: string) => ({ b: `${a}` }), '6');
|
|
39
|
+
// useCleanState((a: number, c?: string) => ({ b: `${a}` }));
|
|
40
|
+
// useCleanState((a: number, c: string) => ({ b: a + c, f: true }), 6, 7);
|
|
41
|
+
// useCleanState({ d: 5000 }, true);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module CleanState
|
|
3
|
+
*/
|
|
4
|
+
import '../../globals';
|
|
5
|
+
export { CleanState } from './class';
|
|
6
|
+
export { useCleanState } from './hooks';
|
|
7
|
+
export type { TCleanState, TStateData, ExtractCleanStateData } from './hook-types';
|
|
8
|
+
export type { SF, StateFragment } from './class-types';
|
|
9
|
+
export * as MergedState from '../../base/merged-state';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module CleanState
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
22
|
+
if (mod && mod.__esModule) return mod;
|
|
23
|
+
var result = {};
|
|
24
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
25
|
+
__setModuleDefault(result, mod);
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.MergedState = exports.useCleanState = exports.CleanState = void 0;
|
|
30
|
+
require("../../globals");
|
|
31
|
+
var class_1 = require("./class");
|
|
32
|
+
Object.defineProperty(exports, "CleanState", { enumerable: true, get: function () { return class_1.CleanState; } });
|
|
33
|
+
var hooks_1 = require("./hooks");
|
|
34
|
+
Object.defineProperty(exports, "useCleanState", { enumerable: true, get: function () { return hooks_1.useCleanState; } });
|
|
35
|
+
exports.MergedState = __importStar(require("../../base/merged-state"));
|