hyperscript-rxjs 1.3.14 → 1.3.15
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/package.json +13 -22
- package/src/array/advance.d.ts +9 -0
- package/src/array/advance.js +13 -0
- package/src/array/advance.test.js +12 -0
- package/src/array/arrayInsert.d.ts +8 -0
- package/src/array/arrayInsert.js +13 -0
- package/src/array/arrayInsert.test.js +13 -0
- package/src/array/arrayRemove.d.ts +7 -0
- package/src/array/arrayRemove.js +15 -0
- package/src/array/arrayRemove.test.js +13 -0
- package/src/array/findLastIndex.d.ts +14 -0
- package/src/array/findLastIndex.js +20 -0
- package/src/array/findLastIndex.test.js +41 -0
- package/src/array/index.d.ts +9 -0
- package/src/array/index.js +9 -0
- package/src/array/isRange.d.ts +7 -0
- package/src/array/isRange.js +15 -0
- package/src/array/isRange.test.js +6 -0
- package/src/array/rangeArray.d.ts +7 -0
- package/src/array/rangeArray.js +10 -0
- package/src/array/rangeArray.test.js +11 -0
- package/src/array/unwrapArgs.d.ts +10 -0
- package/src/array/unwrapArgs.js +15 -0
- package/src/array/unwrapArgs.test.js +33 -0
- package/src/array/zipArray.d.ts +11 -0
- package/src/array/zipArray.js +24 -0
- package/src/array/zipArray.test.js +16 -0
- package/src/comparers/Comparer.d.ts +101 -0
- package/src/comparers/Comparer.js +149 -0
- package/src/comparers/comparers.d.ts +21 -0
- package/src/comparers/comparers.js +10 -0
- package/src/comparers/differenceSet.d.ts +20 -0
- package/src/comparers/differenceSet.js +35 -0
- package/src/comparers/differenceSet.test.js +11 -0
- package/src/comparers/distinctArray.d.ts +13 -0
- package/src/comparers/distinctArray.js +30 -0
- package/src/comparers/distinctArray.test.js +10 -0
- package/src/comparers/findIndexInSet.d.ts +20 -0
- package/src/comparers/findIndexInSet.js +27 -0
- package/src/comparers/findIndexInSet.test.js +8 -0
- package/src/comparers/groupArrayBy.d.ts +19 -0
- package/src/comparers/groupArrayBy.js +29 -0
- package/src/comparers/groupArrayBy.test.js +38 -0
- package/src/comparers/groupSortedEntries.d.ts +17 -0
- package/src/comparers/groupSortedEntries.js +38 -0
- package/src/comparers/groupSortedEntries.test.js +46 -0
- package/src/comparers/index.d.ts +14 -0
- package/src/comparers/index.js +14 -0
- package/src/comparers/intersectSet.d.ts +19 -0
- package/src/comparers/intersectSet.js +35 -0
- package/src/comparers/intersectSet.test.js +14 -0
- package/src/comparers/isEqualset.d.ts +22 -0
- package/src/comparers/isEqualset.js +33 -0
- package/src/comparers/isEqualset.test.js +22 -0
- package/src/comparers/isSubset.d.ts +21 -0
- package/src/comparers/isSubset.js +33 -0
- package/src/comparers/isSubset.test.js +21 -0
- package/src/comparers/isSuperset.d.ts +21 -0
- package/src/comparers/isSuperset.js +13 -0
- package/src/comparers/isSuperset.test.js +21 -0
- package/src/comparers/sortedArrayToSet.d.ts +20 -0
- package/src/comparers/sortedArrayToSet.js +35 -0
- package/src/comparers/sortedArrayToSet.test.js +11 -0
- package/src/comparers/unionSet.d.ts +21 -0
- package/src/comparers/unionSet.js +34 -0
- package/src/comparers/unionSet.test.js +11 -0
- package/src/comparison/compareDate.d.ts +8 -0
- package/src/comparison/compareDate.js +11 -0
- package/src/comparison/compareEntries.d.ts +13 -0
- package/src/comparison/compareEntries.js +13 -0
- package/src/comparison/compareKey.d.ts +11 -0
- package/src/comparison/compareKey.js +25 -0
- package/src/comparison/compareKey.test.js +21 -0
- package/src/comparison/compareKeyPath.d.ts +15 -0
- package/src/comparison/compareKeyPath.js +33 -0
- package/src/comparison/compareKeyPath.test.js +28 -0
- package/src/comparison/compareNumber.d.ts +11 -0
- package/src/comparison/compareNumber.js +27 -0
- package/src/comparison/compareNumber.test.js +21 -0
- package/src/comparison/defaultCompare.d.ts +8 -0
- package/src/comparison/defaultCompare.js +12 -0
- package/src/comparison/defaultCompare.test.js +24 -0
- package/src/comparison/index.d.ts +7 -0
- package/src/comparison/index.js +7 -0
- package/src/comparison/infinity.test.js +122 -0
- package/src/comparison/typeof.test.js +64 -0
- package/src/comparison/types.d.ts +5 -0
- package/src/comparison/types.js +11 -0
- package/src/deep/Deep.d.ts +58 -0
- package/src/deep/Deep.js +267 -0
- package/src/deep/Deep.test.js +130 -0
- package/src/deep/deepCombineLatest.test.js +36 -0
- package/src/deep/deepMerge.test.js +34 -0
- package/src/deep/differenceDeep.test.js +31 -0
- package/src/deep/freshValueDeep.test.js +17 -0
- package/src/deep/index.d.ts +1 -0
- package/src/deep/index.js +2 -0
- package/src/deep/intersectDeep.test.js +25 -0
- package/src/deep/intersectEntries.d.ts +13 -0
- package/src/deep/intersectEntries.js +37 -0
- package/src/deep/intersectEntries.test.js +20 -0
- package/src/deep/objectToDeep.test.js +31 -0
- package/src/deep/replaceValueDeep.test.js +21 -0
- package/src/deep/unionDeep.test.js +30 -0
- package/src/deep/zipValueDeep.test.js +21 -0
- package/src/deep-rxjs/ObservableArray.d.ts +55 -0
- package/src/deep-rxjs/ObservableArray.js +94 -0
- package/src/deep-rxjs/ObservableArray.test.js +117 -0
- package/src/deep-rxjs/index.d.ts +2 -0
- package/src/deep-rxjs/index.js +2 -0
- package/src/deep-rxjs/isRxType.d.ts +9 -0
- package/src/deep-rxjs/isRxType.js +15 -0
- package/src/deep-rxjs/isRxType.test.js +43 -0
- package/src/hyperscript-rxjs/HyperscriptExtensions.d.ts +20 -0
- package/src/hyperscript-rxjs/checkbox.d.ts +13 -0
- package/src/hyperscript-rxjs/checkbox.js +47 -0
- package/src/hyperscript-rxjs/checkbox.test.js +68 -0
- package/src/hyperscript-rxjs/choice.d.ts +13 -0
- package/src/hyperscript-rxjs/choice.js +24 -0
- package/src/hyperscript-rxjs/choice.test.js +108 -0
- package/src/hyperscript-rxjs/collapse.d.ts +14 -0
- package/src/hyperscript-rxjs/collapse.js +32 -0
- package/src/hyperscript-rxjs/collapse.test.js +67 -0
- package/src/hyperscript-rxjs/displays/blockLevelFamily.d.ts +5 -0
- package/src/hyperscript-rxjs/displays/blockLevelFamily.js +51 -0
- package/src/hyperscript-rxjs/displays/getDisplay.d.ts +7 -0
- package/src/hyperscript-rxjs/displays/getDisplay.js +51 -0
- package/src/hyperscript-rxjs/displays/getDisplay.test.js +56 -0
- package/src/hyperscript-rxjs/displays/index.d.ts +3 -0
- package/src/hyperscript-rxjs/displays/index.js +3 -0
- package/src/hyperscript-rxjs/displays/inlineFamily.d.ts +5 -0
- package/src/hyperscript-rxjs/displays/inlineFamily.js +73 -0
- package/src/hyperscript-rxjs/flip.d.ts +15 -0
- package/src/hyperscript-rxjs/flip.js +29 -0
- package/src/hyperscript-rxjs/flip.test.js +85 -0
- package/src/hyperscript-rxjs/fragment.d.ts +10 -0
- package/src/hyperscript-rxjs/fragment.js +22 -0
- package/src/hyperscript-rxjs/fragment.test.js +70 -0
- package/src/hyperscript-rxjs/hyperscript.d.ts +15 -0
- package/src/hyperscript-rxjs/hyperscript.js +170 -0
- package/src/hyperscript-rxjs/hyperscript.test.js +75 -0
- package/src/hyperscript-rxjs/index.d.ts +19 -0
- package/src/hyperscript-rxjs/index.js +19 -0
- package/src/hyperscript-rxjs/multiselect.d.ts +18 -0
- package/src/hyperscript-rxjs/multiselect.js +41 -0
- package/src/hyperscript-rxjs/multiselect.test.js +121 -0
- package/src/hyperscript-rxjs/numberbox.d.ts +14 -0
- package/src/hyperscript-rxjs/numberbox.js +73 -0
- package/src/hyperscript-rxjs/numberbox.test.js +84 -0
- package/src/hyperscript-rxjs/radio.d.ts +15 -0
- package/src/hyperscript-rxjs/radio.js +53 -0
- package/src/hyperscript-rxjs/radio.test.js +59 -0
- package/src/hyperscript-rxjs/select.d.ts +28 -0
- package/src/hyperscript-rxjs/select.js +88 -0
- package/src/hyperscript-rxjs/select.test.js +101 -0
- package/src/hyperscript-rxjs/tabControls/bindTabIndex.d.ts +12 -0
- package/src/hyperscript-rxjs/tabControls/bindTabIndex.js +59 -0
- package/src/hyperscript-rxjs/tabControls/index.d.ts +8 -0
- package/src/hyperscript-rxjs/tabControls/index.js +10 -0
- package/src/hyperscript-rxjs/tabControls/tabControl.d.ts +19 -0
- package/src/hyperscript-rxjs/tabControls/tabControl.js +40 -0
- package/src/hyperscript-rxjs/tabControls/tabControl.test.js +98 -0
- package/src/hyperscript-rxjs/tabControls/tabNavItem.d.ts +9 -0
- package/src/hyperscript-rxjs/tabControls/tabNavItem.js +30 -0
- package/src/hyperscript-rxjs/tabControls/tabPanel.d.ts +9 -0
- package/src/hyperscript-rxjs/tabControls/tabPanel.js +21 -0
- package/src/hyperscript-rxjs/tabControls/tabRoot.d.ts +7 -0
- package/src/hyperscript-rxjs/tabControls/tabRoot.js +26 -0
- package/src/hyperscript-rxjs/tags.d.ts +193 -0
- package/src/hyperscript-rxjs/tags.js +751 -0
- package/src/hyperscript-rxjs/tags.test.js +75 -0
- package/src/hyperscript-rxjs/textNode.d.ts +11 -0
- package/src/hyperscript-rxjs/textNode.js +51 -0
- package/src/hyperscript-rxjs/textNode.test.js +56 -0
- package/src/hyperscript-rxjs/textarea.d.ts +17 -0
- package/src/hyperscript-rxjs/textarea.js +45 -0
- package/src/hyperscript-rxjs/textarea.test.js +52 -0
- package/src/hyperscript-rxjs/textbox.d.ts +15 -0
- package/src/hyperscript-rxjs/textbox.js +42 -0
- package/src/hyperscript-rxjs/textbox.test.js +52 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +19 -0
- package/src/nodes/attachSubscriptionToNode.d.ts +13 -0
- package/src/nodes/attachSubscriptionToNode.js +25 -0
- package/src/nodes/attachSubscriptionToNode.test.js +73 -0
- package/src/nodes/index.d.ts +6 -0
- package/src/nodes/index.js +6 -0
- package/src/nodes/normalizeChildNodes.d.ts +9 -0
- package/src/nodes/normalizeChildNodes.js +15 -0
- package/src/nodes/normalizeChildNodes.test.js +55 -0
- package/src/nodes/parseHyperscriptArgs.d.ts +10 -0
- package/src/nodes/parseHyperscriptArgs.js +57 -0
- package/src/nodes/parseHyperscriptArgs.test.js +85 -0
- package/src/nodes/pipeEvent.d.ts +15 -0
- package/src/nodes/pipeEvent.js +49 -0
- package/src/nodes/pipeEvent.test.js +97 -0
- package/src/nodes/subscribeEvent.d.ts +15 -0
- package/src/nodes/subscribeEvent.js +56 -0
- package/src/nodes/subscribeEvent.test.js +88 -0
- package/src/object/index.d.ts +10 -0
- package/src/object/index.js +11 -0
- package/src/object/intersectObject.d.ts +12 -0
- package/src/object/intersectObject.js +23 -0
- package/src/object/intersectObject.test.js +69 -0
- package/src/object/isEmptyObject.d.ts +7 -0
- package/src/object/isEmptyObject.js +13 -0
- package/src/object/isEmptyObject.test.js +33 -0
- package/src/object/isPlainObject.d.ts +11 -0
- package/src/object/isPlainObject.js +18 -0
- package/src/object/nestedCombineLatest.d.ts +11 -0
- package/src/object/nestedCombineLatest.js +18 -0
- package/src/object/nestedCombineLatest.test.js +25 -0
- package/src/object/nestedMerge.d.ts +11 -0
- package/src/object/nestedMerge.js +11 -0
- package/src/object/nestedMerge.test.js +61 -0
- package/src/object/pickBehaviorSubject.d.ts +13 -0
- package/src/object/pickBehaviorSubject.js +81 -0
- package/src/object/pickBehaviorSubject.test.js +88 -0
- package/src/object/pluckProperty.d.ts +13 -0
- package/src/object/pluckProperty.js +24 -0
- package/src/object/pluckProperty.test.js +37 -0
- package/src/object/restore.d.ts +12 -0
- package/src/object/restore.js +69 -0
- package/src/object/restore.test.js +124 -0
- package/src/object/splitObjectByObservable.d.ts +12 -0
- package/src/object/splitObjectByObservable.js +41 -0
- package/src/object/splitObjectByObservable.test.js +78 -0
- package/src/props/getNestedProperty.d.ts +12 -0
- package/src/props/getNestedProperty.js +31 -0
- package/src/props/getNestedProperty.test.js +72 -0
- package/src/props/index.d.ts +7 -0
- package/src/props/index.js +7 -0
- package/src/props/parsePropName.d.ts +13 -0
- package/src/props/parsePropName.js +45 -0
- package/src/props/parsePropName.test.js +67 -0
- package/src/props/setProp.d.ts +16 -0
- package/src/props/setProp.js +42 -0
- package/src/props/setProp.test.js +59 -0
- package/src/props/setProps.d.ts +14 -0
- package/src/props/setProps.js +47 -0
- package/src/props/setProps.test.js +97 -0
- package/src/props/subscribeProp.d.ts +16 -0
- package/src/props/subscribeProp.js +47 -0
- package/src/props/subscribeProp.test.js +81 -0
- package/src/ramda/compose.d.ts +10 -0
- package/src/ramda/compose.js +36 -0
- package/src/ramda/compose.test.js +73 -0
- package/src/ramda/cond.d.ts +12 -0
- package/src/ramda/cond.js +29 -0
- package/src/ramda/cond.test.js +88 -0
- package/src/ramda/fold.d.ts +13 -0
- package/src/ramda/fold.js +20 -0
- package/src/ramda/fold.test.js +51 -0
- package/src/ramda/index.d.ts +6 -0
- package/src/ramda/index.js +6 -0
- package/src/ramda/pipe.d.ts +13 -0
- package/src/ramda/pipe.js +27 -0
- package/src/ramda/pipe.test.js +77 -0
- package/src/ramda/unfold.d.ts +11 -0
- package/src/ramda/unfold.js +20 -0
- package/src/ramda/unfold.test.js +29 -0
- package/src/unquoted-json/ajax.test.js +1074 -0
- package/src/unquoted-json/index.d.ts +13 -0
- package/src/unquoted-json/index.js +12 -0
- package/src/unquoted-json/queryStringify.d.ts +8 -0
- package/src/unquoted-json/queryStringify.js +70 -0
- package/src/unquoted-json/queryStringify.test.js +110 -0
- package/src/unquoted-json/stringifyKey.d.ts +7 -0
- package/src/unquoted-json/stringifyKey.js +16 -0
- package/src/unquoted-json/stringifyKey.test.js +51 -0
- package/src/unquoted-json/stringifyStringValue.d.ts +7 -0
- package/src/unquoted-json/stringifyStringValue.js +17 -0
- package/src/unquoted-json/stringifyStringValue.test.js +52 -0
- package/src/unquoted-json/unquotedJsonStringify.d.ts +7 -0
- package/src/unquoted-json/unquotedJsonStringify.js +39 -0
- package/src/unquoted-json/unquotedJsonStringify.test.js +52 -0
- package/dist/hyperscript-rxjs.d.ts +0 -1412
- package/dist/hyperscript-rxjs.js +0 -1
- package/dist/tsdoc-metadata.json +0 -11
@@ -0,0 +1,47 @@
|
|
1
|
+
import { splitObjectByObservable } from '../object/splitObjectByObservable';
|
2
|
+
import { setProp } from './setProp';
|
3
|
+
import { subscribeProp } from './subscribeProp';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* 为 DOM 元素设置属性,包括静态属性和动态属性(Observable)。
|
7
|
+
*
|
8
|
+
* @param {HTMLElement} element - 要设置属性的 DOM 元素。
|
9
|
+
* @param {Object} props - 包含属性键值对的对象,其中值可以是普通值或 Observable。
|
10
|
+
* @returns {HTMLElement} - 返回元素本身,支持链式调用。
|
11
|
+
* @throws {Error} 如果参数无效或设置属性失败。
|
12
|
+
*/
|
13
|
+
export function setProps(element, props) {
|
14
|
+
// 参数检查
|
15
|
+
if (!(element instanceof HTMLElement)) {
|
16
|
+
throw new Error('参数 "element" 必须是一个有效的 DOM 元素。');
|
17
|
+
}
|
18
|
+
if (typeof props !== 'object' || props === null) {
|
19
|
+
throw new Error('参数 "props" 必须是一个非空对象。');
|
20
|
+
}
|
21
|
+
|
22
|
+
// 将属性分为 Observable 和普通值
|
23
|
+
const [observables, scalars] = splitObjectByObservable(props);
|
24
|
+
|
25
|
+
// 设置普通属性
|
26
|
+
Object.entries(scalars)
|
27
|
+
.forEach(([key, value]) => {
|
28
|
+
try {
|
29
|
+
setProp(element, key, value);
|
30
|
+
} catch (error) {
|
31
|
+
console.error(`设置属性 "${key}" 时发生错误:`, error);
|
32
|
+
}
|
33
|
+
});
|
34
|
+
|
35
|
+
// 注意顺序,订阅动态属性要在设置静态属性之后
|
36
|
+
Object.entries(observables)
|
37
|
+
.forEach(([key, value$]) => {
|
38
|
+
try {
|
39
|
+
subscribeProp(element, key, value$);
|
40
|
+
} catch (error) {
|
41
|
+
console.error(`订阅属性 "${key}" 时发生错误:`, error);
|
42
|
+
}
|
43
|
+
});
|
44
|
+
|
45
|
+
// 返回元素本身,支持链式调用
|
46
|
+
return element;
|
47
|
+
}
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import { setProps } from './setProps';
|
2
|
+
import { setProp } from './setProp';
|
3
|
+
import { subscribeProp } from './subscribeProp';
|
4
|
+
import { of } from 'rxjs';
|
5
|
+
|
6
|
+
jest.mock('./setProp');
|
7
|
+
jest.mock('./subscribeProp');
|
8
|
+
|
9
|
+
describe('setProps', () => {
|
10
|
+
let div;
|
11
|
+
|
12
|
+
beforeEach(() => {
|
13
|
+
div = document.createElement('div');
|
14
|
+
jest.clearAllMocks();
|
15
|
+
});
|
16
|
+
|
17
|
+
test('should set static properties correctly', () => {
|
18
|
+
const props = {
|
19
|
+
id: 'myDiv',
|
20
|
+
className: 'container',
|
21
|
+
style: { color: 'red' }
|
22
|
+
};
|
23
|
+
|
24
|
+
setProps(div, props);
|
25
|
+
|
26
|
+
expect(setProp).toHaveBeenCalledWith(div, 'id', 'myDiv');
|
27
|
+
expect(setProp).toHaveBeenCalledWith(div, 'className', 'container');
|
28
|
+
expect(setProp).toHaveBeenCalledWith(div, 'style', { color: 'red' });
|
29
|
+
});
|
30
|
+
|
31
|
+
test('should subscribe to observable properties', () => {
|
32
|
+
const props = {
|
33
|
+
textContent$: of('Hello', 'World')
|
34
|
+
};
|
35
|
+
|
36
|
+
setProps(div, props);
|
37
|
+
|
38
|
+
expect(subscribeProp).toHaveBeenCalledWith(div, 'textContent$', expect.any(Object));
|
39
|
+
});
|
40
|
+
|
41
|
+
test('should handle both static and observable properties', () => {
|
42
|
+
const props = {
|
43
|
+
id: 'myDiv',
|
44
|
+
textContent$: of('Hello', 'World')
|
45
|
+
};
|
46
|
+
|
47
|
+
setProps(div, props);
|
48
|
+
|
49
|
+
expect(setProp).toHaveBeenCalledWith(div, 'id', 'myDiv');
|
50
|
+
expect(subscribeProp).toHaveBeenCalledWith(div, 'textContent$', expect.any(Object));
|
51
|
+
});
|
52
|
+
|
53
|
+
test('should throw an error if element is not a valid DOM element', () => {
|
54
|
+
const props = { id: 'myDiv' };
|
55
|
+
|
56
|
+
expect(() => setProps(null, props)).toThrow('参数 "element" 必须是一个有效的 DOM 元素。');
|
57
|
+
expect(() => setProps({}, props)).toThrow('参数 "element" 必须是一个有效的 DOM 元素。');
|
58
|
+
});
|
59
|
+
|
60
|
+
test('should throw an error if props is not a valid object', () => {
|
61
|
+
expect(() => setProps(div, null)).toThrow('参数 "props" 必须是一个非空对象。');
|
62
|
+
expect(() => setProps(div, 'invalid')).toThrow('参数 "props" 必须是一个非空对象。');
|
63
|
+
});
|
64
|
+
|
65
|
+
test('should log errors when setProp fails', () => {
|
66
|
+
const props = { id: 'myDiv' };
|
67
|
+
setProp.mockImplementation(() => {
|
68
|
+
throw new Error('Failed to set property');
|
69
|
+
});
|
70
|
+
|
71
|
+
console.error = jest.fn();
|
72
|
+
|
73
|
+
setProps(div, props);
|
74
|
+
|
75
|
+
expect(console.error).toHaveBeenCalledWith('设置属性 "id" 时发生错误:', expect.any(Error));
|
76
|
+
});
|
77
|
+
|
78
|
+
test('should log errors when subscribeProp fails', () => {
|
79
|
+
const props = { textContent$: of('Hello') };
|
80
|
+
subscribeProp.mockImplementation(() => {
|
81
|
+
throw new Error('Failed to subscribe to property');
|
82
|
+
});
|
83
|
+
|
84
|
+
console.error = jest.fn();
|
85
|
+
|
86
|
+
setProps(div, props);
|
87
|
+
|
88
|
+
expect(console.error).toHaveBeenCalledWith('订阅属性 "textContent$" 时发生错误:', expect.any(Error));
|
89
|
+
});
|
90
|
+
|
91
|
+
test('should return the element itself for chainable calls', () => {
|
92
|
+
const props = { id: 'myDiv' };
|
93
|
+
const result = setProps(div, props);
|
94
|
+
|
95
|
+
expect(result).toBe(div);
|
96
|
+
});
|
97
|
+
});
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { Observable } from 'rxjs';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* 订阅一个 Observable,将其发出的值动态设置为指定 DOM 元素的属性值。
|
5
|
+
*
|
6
|
+
* @param element - 要设置属性的 DOM 元素。
|
7
|
+
* @param key - 属性名称,支持普通属性、嵌套属性(如 "style.color")或以 "." 开头的类名(如 ".active")。
|
8
|
+
* @param value$ - 一个 RxJS Observable ,用于发出属性值。
|
9
|
+
* @returns 返回元素本身,支持链式调用。
|
10
|
+
* @throws 如果参数无效或订阅失败会抛出错误。
|
11
|
+
*/
|
12
|
+
export function subscribeProp(
|
13
|
+
element: HTMLElement,
|
14
|
+
key: string,
|
15
|
+
value$: Observable<any>
|
16
|
+
): HTMLElement;
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { startWith } from 'rxjs/operators';
|
2
|
+
import { attachSubscriptionToNode } from '../nodes/attachSubscriptionToNode';
|
3
|
+
import { setProp } from './setProp';
|
4
|
+
import { isObservable, Observable } from 'rxjs';
|
5
|
+
|
6
|
+
/**
|
7
|
+
*
|
8
|
+
* 订阅一个 BehaviorSubject,将其发出的值动态设置为指定 DOM 元素的属性值。
|
9
|
+
*
|
10
|
+
* @template T
|
11
|
+
* @param {HTMLElement} element - 要设置属性的 DOM 元素。
|
12
|
+
* @param {string} key - 属性名称,支持普通属性、嵌套属性(如 "style.color")或以 "." 开头的类名(如 ".active")。
|
13
|
+
* @param {Observable<T>} value$ - 一个 RxJS Observable ,用于发出属性值。
|
14
|
+
* @returns {HTMLElement} - 返回元素本身,支持链式调用。
|
15
|
+
* @throws {Error} 如果参数无效或订阅失败。
|
16
|
+
*/
|
17
|
+
export function subscribeProp(element, key, value$) {
|
18
|
+
// 参数检查,确保输入有效
|
19
|
+
if (!(element instanceof HTMLElement)) {
|
20
|
+
throw new Error('参数 "element" 必须是一个有效的 DOM 元素。');
|
21
|
+
}
|
22
|
+
if (typeof key !== 'string') {
|
23
|
+
throw new Error('参数 "key" 必须是一个字符串。');
|
24
|
+
}
|
25
|
+
if (!isObservable(value$)) {
|
26
|
+
throw new Error('参数 "value$" 必须是一个有效的 Observable。');
|
27
|
+
}
|
28
|
+
|
29
|
+
// 创建订阅,将 BehaviorSubject 的值动态设置为元素的属性
|
30
|
+
const subscription =
|
31
|
+
value$.pipe(
|
32
|
+
startWith(0)
|
33
|
+
).subscribe({
|
34
|
+
next: value => {
|
35
|
+
setProp(element, key, value);
|
36
|
+
},
|
37
|
+
error: e => {
|
38
|
+
setProp(element, key, e.message);
|
39
|
+
}
|
40
|
+
});
|
41
|
+
|
42
|
+
// 将订阅附加到 DOM 节点,便于统一管理
|
43
|
+
attachSubscriptionToNode(element, subscription);
|
44
|
+
|
45
|
+
// 返回元素本身,支持链式调用
|
46
|
+
return element;
|
47
|
+
}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import { of, throwError } from 'rxjs';
|
2
|
+
import { delay } from 'rxjs/operators';
|
3
|
+
import { subscribeProp } from './subscribeProp';
|
4
|
+
import { setProp } from './setProp';
|
5
|
+
import { attachSubscriptionToNode } from '../nodes/attachSubscriptionToNode';
|
6
|
+
|
7
|
+
jest.mock('./setProp');
|
8
|
+
jest.mock('../nodes/attachSubscriptionToNode');
|
9
|
+
|
10
|
+
describe('subscribeProp', () => {
|
11
|
+
let div;
|
12
|
+
|
13
|
+
beforeEach(() => {
|
14
|
+
div = document.createElement('div');
|
15
|
+
jest.clearAllMocks();
|
16
|
+
});
|
17
|
+
|
18
|
+
test('should set a property dynamically based on Observable values', (done) => {
|
19
|
+
const value$ = of('red', 'blue', 'green').pipe(delay(10));
|
20
|
+
subscribeProp(div, 'style.color', value$);
|
21
|
+
|
22
|
+
setTimeout(() => {
|
23
|
+
expect(setProp).toHaveBeenCalledWith(div, 'style.color', 'red');
|
24
|
+
expect(setProp).toHaveBeenCalledWith(div, 'style.color', 'blue');
|
25
|
+
expect(setProp).toHaveBeenCalledWith(div, 'style.color', 'green');
|
26
|
+
done();
|
27
|
+
}, 50);
|
28
|
+
});
|
29
|
+
|
30
|
+
test('should handle errors in Observable and set error message as property', (done) => {
|
31
|
+
const error$ = throwError(() => new Error('Something went wrong')).pipe(delay(10));
|
32
|
+
subscribeProp(div, 'textContent', error$);
|
33
|
+
|
34
|
+
setTimeout(() => {
|
35
|
+
expect(setProp).toHaveBeenCalledWith(div, 'textContent', 'Something went wrong');
|
36
|
+
done();
|
37
|
+
}, 20);
|
38
|
+
});
|
39
|
+
|
40
|
+
test('should attach subscription to the DOM node', () => {
|
41
|
+
const value$ = of('test');
|
42
|
+
subscribeProp(div, 'id', value$);
|
43
|
+
|
44
|
+
expect(attachSubscriptionToNode).toHaveBeenCalledTimes(1);
|
45
|
+
expect(attachSubscriptionToNode).toHaveBeenCalledWith(
|
46
|
+
div,
|
47
|
+
expect.any(Object) // 确保传递的是一个订阅对象
|
48
|
+
);
|
49
|
+
});
|
50
|
+
|
51
|
+
test('should throw an error if element is not a valid DOM element', () => {
|
52
|
+
const value$ = of('test');
|
53
|
+
expect(() => subscribeProp(null, 'id', value$)).toThrow(
|
54
|
+
'参数 "element" 必须是一个有效的 DOM 元素。'
|
55
|
+
);
|
56
|
+
expect(() => subscribeProp({}, 'id', value$)).toThrow(
|
57
|
+
'参数 "element" 必须是一个有效的 DOM 元素。'
|
58
|
+
);
|
59
|
+
});
|
60
|
+
|
61
|
+
test('should throw an error if key is not a string', () => {
|
62
|
+
const value$ = of('test');
|
63
|
+
expect(() => subscribeProp(div, null, value$)).toThrow('参数 "key" 必须是一个字符串。');
|
64
|
+
expect(() => subscribeProp(div, 123, value$)).toThrow('参数 "key" 必须是一个字符串。');
|
65
|
+
});
|
66
|
+
|
67
|
+
test('should throw an error if value$ is not a valid Observable', () => {
|
68
|
+
expect(() => subscribeProp(div, 'id', null)).toThrow(
|
69
|
+
'参数 "value$" 必须是一个有效的 Observable。'
|
70
|
+
);
|
71
|
+
expect(() => subscribeProp(div, 'id', {})).toThrow(
|
72
|
+
'参数 "value$" 必须是一个有效的 Observable。'
|
73
|
+
);
|
74
|
+
});
|
75
|
+
|
76
|
+
test('should support chainable calls', () => {
|
77
|
+
const value$ = of('test');
|
78
|
+
const result = subscribeProp(div, 'id', value$);
|
79
|
+
expect(result).toBe(div);
|
80
|
+
});
|
81
|
+
});
|
@@ -0,0 +1,36 @@
|
|
1
|
+
/**
|
2
|
+
* 函数优先的管道组合工具(从右向左执行)
|
3
|
+
* @public
|
4
|
+
* @param {...function(*): *} fns - 要组合的函数列表
|
5
|
+
* @returns {function(*): *} 组合后的函数(无参数时返回恒等函数 x => x)
|
6
|
+
* @throws {TypeError} 当参数包含非函数时抛出错误
|
7
|
+
*/
|
8
|
+
export const compose = (...fns) => {
|
9
|
+
if (!fns || fns.length === 0) {
|
10
|
+
/** @param {*} x */
|
11
|
+
return x => x;
|
12
|
+
}
|
13
|
+
|
14
|
+
const firstNonFunctionIndex = fns.findIndex(fn => typeof fn !== 'function');
|
15
|
+
if (firstNonFunctionIndex !== -1) {
|
16
|
+
const invalidFn = fns[firstNonFunctionIndex];
|
17
|
+
const errorType =
|
18
|
+
invalidFn === null ? 'null' :
|
19
|
+
invalidFn === undefined ? 'undefined' :
|
20
|
+
typeof invalidFn;
|
21
|
+
throw new TypeError(
|
22
|
+
`所有参数必须是函数,但第 ${firstNonFunctionIndex} 个参数是: ${errorType}`
|
23
|
+
);
|
24
|
+
}
|
25
|
+
|
26
|
+
if (fns.length === 1) {
|
27
|
+
return fns[0];
|
28
|
+
}
|
29
|
+
|
30
|
+
// 标注 args 类型
|
31
|
+
return fns.reduce(
|
32
|
+
(acc, fn) =>
|
33
|
+
/** @returns {*} */
|
34
|
+
(...args) => acc(fn(...args))
|
35
|
+
);
|
36
|
+
};
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import { compose } from './compose';
|
2
|
+
|
3
|
+
describe('compose function', () => {
|
4
|
+
describe('基础功能', () => {
|
5
|
+
test('空参数时返回恒等函数', () => {
|
6
|
+
const identity = compose();
|
7
|
+
expect(identity(42)).toBe(42);
|
8
|
+
expect(identity('hello')).toBe('hello');
|
9
|
+
expect(identity(null)).toBeNull();
|
10
|
+
expect(identity(undefined)).toBeUndefined();
|
11
|
+
});
|
12
|
+
|
13
|
+
test('单函数时直接返回该函数', () => {
|
14
|
+
const mockFn = jest.fn(x => x * 2);
|
15
|
+
const result = compose(mockFn);
|
16
|
+
expect(result).toBe(mockFn);
|
17
|
+
expect(result(5)).toBe(10);
|
18
|
+
expect(mockFn).toHaveBeenCalledWith(5);
|
19
|
+
});
|
20
|
+
|
21
|
+
test('正确执行从右向左的函数组合', () => {
|
22
|
+
const add1 = x => x + 1;
|
23
|
+
const square = x => x * x;
|
24
|
+
const double = x => x * 2;
|
25
|
+
|
26
|
+
const composed = compose(add1, square, double);
|
27
|
+
expect(composed(3)).toBe(37); // (3*2)^2 + 1 = 37
|
28
|
+
});
|
29
|
+
|
30
|
+
test('处理多参数函数', () => {
|
31
|
+
const sum = (a, b) => a + b;
|
32
|
+
const triple = x => x * 3;
|
33
|
+
|
34
|
+
const composed = compose(triple, sum);
|
35
|
+
expect(composed(2, 4)).toBe(18); // (2+4)*3
|
36
|
+
});
|
37
|
+
});
|
38
|
+
|
39
|
+
describe('类型检查', () => {
|
40
|
+
test('检测第一个非函数参数并报错', () => {
|
41
|
+
const errorCases = [
|
42
|
+
{ input: [42], expected: '第 0 个参数是: number' },
|
43
|
+
{ input: [x => x, null], expected: '第 1 个参数是: null' },
|
44
|
+
{ input: [undefined, x => x], expected: '第 0 个参数是: undefined' },
|
45
|
+
{ input: ['text', x => x], expected: '第 0 个参数是: string' },
|
46
|
+
{ input: [{}, x => x], expected: '第 0 个参数是: object' },
|
47
|
+
{ input: [[], x => x], expected: '第 0 个参数是: object' }
|
48
|
+
];
|
49
|
+
|
50
|
+
errorCases.forEach(({ input, expected }) => {
|
51
|
+
expect(() => compose(...input)).toThrow(TypeError);
|
52
|
+
expect(() => compose(...input)).toThrow(expected);
|
53
|
+
});
|
54
|
+
});
|
55
|
+
|
56
|
+
test('混合类型参数中检测第一个错误', () => {
|
57
|
+
const add1 = x => x + 1;
|
58
|
+
expect(() => compose(add1, 'text', 42, null))
|
59
|
+
.toThrow('第 1 个参数是: string');
|
60
|
+
});
|
61
|
+
});
|
62
|
+
|
63
|
+
describe('边界情况', () => {
|
64
|
+
test('处理返回undefined的函数', () => {
|
65
|
+
const noop = () => undefined;
|
66
|
+
const toString = x => String(x);
|
67
|
+
|
68
|
+
const composed = compose(toString, noop);
|
69
|
+
expect(composed(123)).toBe('undefined');
|
70
|
+
});
|
71
|
+
|
72
|
+
});
|
73
|
+
});
|
@@ -0,0 +1,12 @@
|
|
1
|
+
/**
|
2
|
+
* 条件分支函数,根据分支依次判断并执行。
|
3
|
+
* @public
|
4
|
+
* @param branches - 分支数组,每个分支可以是函数或 [predicate, action] 形式的数组。
|
5
|
+
* @returns 一个函数,调用时会依次判断分支并返回第一个匹配的结果。
|
6
|
+
*/
|
7
|
+
export function cond<T extends any[], R>(
|
8
|
+
branches: (
|
9
|
+
| ((...args: T) => R | undefined | void | null | false)
|
10
|
+
| [ (...args: T) => any, (...args: [...T, any]) => R ]
|
11
|
+
)[]
|
12
|
+
): (...args: T) => R | undefined;
|
@@ -0,0 +1,29 @@
|
|
1
|
+
/**
|
2
|
+
* 条件分支函数,依次判断分支并返回第一个匹配的结果。
|
3
|
+
*
|
4
|
+
* @public
|
5
|
+
* @param {Array<Function|[Function, Function]>} branches - 分支数组,每个分支可以是函数或 [谓词函数, 动作函数] 形式
|
6
|
+
* @returns {function(...*): *} 组合后的条件函数
|
7
|
+
*/
|
8
|
+
export const cond = function cond(branches) {
|
9
|
+
/**
|
10
|
+
* @this any
|
11
|
+
*/
|
12
|
+
return function () {
|
13
|
+
for (const branch of branches) {
|
14
|
+
if (typeof branch === 'function') {
|
15
|
+
let tryAction = branch.apply(this, arguments)
|
16
|
+
if (tryAction) {
|
17
|
+
return tryAction;
|
18
|
+
}
|
19
|
+
} else if (Array.isArray(branch)) {
|
20
|
+
let predicate = branch[0].apply(this, arguments)
|
21
|
+
if (predicate) {
|
22
|
+
let args = Array.from(arguments)
|
23
|
+
args[args.length] = predicate
|
24
|
+
return branch[1].apply(this, args);
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import { cond } from './cond'
|
2
|
+
|
3
|
+
describe('cond', () => {
|
4
|
+
test('returns a function', () => {
|
5
|
+
let y = typeof cond([])
|
6
|
+
expect(y).toEqual('function');
|
7
|
+
});
|
8
|
+
|
9
|
+
test('returns a conditional function', () => {
|
10
|
+
let fn = cond([
|
11
|
+
[x => x === 0, x => ('water freezes at 0°C')],
|
12
|
+
[x => x === 100, x => ('water boils at 100°C')],
|
13
|
+
[x => true, function (temp) { return 'nothing special happens at ' + temp + '°C'; }]
|
14
|
+
]);
|
15
|
+
|
16
|
+
expect(fn(0)).toEqual('water freezes at 0°C');
|
17
|
+
expect(fn(50)).toEqual('nothing special happens at 50°C');
|
18
|
+
expect(fn(100)).toEqual('water boils at 100°C');
|
19
|
+
});
|
20
|
+
|
21
|
+
test('returns a function which returns undefined if none of the predicates matches', () => {
|
22
|
+
let fn = cond([
|
23
|
+
[x => x === ('foo'), x => (1)],
|
24
|
+
[x => x === ('bar'), x => (2)]
|
25
|
+
]);
|
26
|
+
expect(fn('quux')).toEqual(undefined);
|
27
|
+
});
|
28
|
+
|
29
|
+
test('predicates are tested in order', () => {
|
30
|
+
let fn = cond([
|
31
|
+
[x => true, x => ('foo')],
|
32
|
+
[x => true, x => ('bar')],
|
33
|
+
[x => true, x => ('baz')]
|
34
|
+
]);
|
35
|
+
expect(fn()).toEqual('foo');
|
36
|
+
});
|
37
|
+
|
38
|
+
test('forwards all arguments to predicates and to transformers', () => {
|
39
|
+
let fn = cond([
|
40
|
+
[function (_, x) { return x === 42; }, (...args) => args]
|
41
|
+
]);
|
42
|
+
expect(fn(21, 42, 84)).toEqual([21, 42, 84, true]);
|
43
|
+
});
|
44
|
+
|
45
|
+
});
|
46
|
+
|
47
|
+
describe('cond new featrues', () => {
|
48
|
+
|
49
|
+
test('just predicate and return result', () => {
|
50
|
+
let fmt = cond([x => x.trim()]);
|
51
|
+
let x = fmt(" ")
|
52
|
+
expect(x).toEqual(undefined)
|
53
|
+
let y = fmt(" x ")
|
54
|
+
expect(y).toEqual("x")
|
55
|
+
let z = fmt("")
|
56
|
+
expect(z).toEqual(undefined)
|
57
|
+
});
|
58
|
+
|
59
|
+
test('cache predicate result', () => {
|
60
|
+
let fmt = cond([
|
61
|
+
[x => x.trim(), (_, res) => ({ res })],
|
62
|
+
]);
|
63
|
+
let y = fmt(" x ")
|
64
|
+
expect(y).toEqual({ res: "x" })
|
65
|
+
let z = fmt("")
|
66
|
+
expect(z).toEqual(undefined)
|
67
|
+
let x = fmt(" ")
|
68
|
+
expect(x).toEqual(undefined)
|
69
|
+
});
|
70
|
+
|
71
|
+
test('an example in world', () => {
|
72
|
+
let token = cond([
|
73
|
+
input => {
|
74
|
+
let mtch = /^\d+/.exec(input)
|
75
|
+
if (mtch && mtch.length > 0) {
|
76
|
+
let lexeme = mtch[0]
|
77
|
+
let restInput = input.slice(lexeme.length)
|
78
|
+
return { token: { number: parseInt(lexeme) }, restInput }
|
79
|
+
} else {
|
80
|
+
return null
|
81
|
+
}
|
82
|
+
}
|
83
|
+
]);
|
84
|
+
let y = token("123+234")
|
85
|
+
expect(y).toEqual({ "restInput": "+234", "token": { "number": 123 } })
|
86
|
+
});
|
87
|
+
|
88
|
+
});
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/**
|
2
|
+
* 通用折叠(归约)函数,支持自定义迭代逻辑。
|
3
|
+
* @public
|
4
|
+
* @param fn - 迭代函数,接收 (acc, seed),返回 [newAcc, nextSeed] 或 [newAcc] 或 null/undefined 结束。
|
5
|
+
* @param acc - 初始累加值。
|
6
|
+
* @param seed - 初始种子值。
|
7
|
+
* @returns 折叠后的累加结果。
|
8
|
+
*/
|
9
|
+
export function fold<A, S>(
|
10
|
+
fn: (acc: A, seed: S) => [A, S] | [A] | [] | null | undefined,
|
11
|
+
acc: A,
|
12
|
+
seed: S
|
13
|
+
): A;
|
@@ -0,0 +1,20 @@
|
|
1
|
+
/**
|
2
|
+
* 通用折叠(递归归约)函数。
|
3
|
+
*
|
4
|
+
* @public
|
5
|
+
* @param {(this:any, acc: any, seed: any) => [*, *]|[any]|[]|null|undefined} fn - 处理函数,返回 [新acc, 新seed] 或 [新acc]
|
6
|
+
* @param {*} acc - 初始累加值
|
7
|
+
* @param {*} seed - 初始种子
|
8
|
+
* @returns {*} 折叠后的结果
|
9
|
+
* @this *
|
10
|
+
*/
|
11
|
+
export function fold(fn, acc, seed) {
|
12
|
+
while (seed != null) {
|
13
|
+
let pair = fn.call(this, acc, seed)
|
14
|
+
if (pair == null || pair.length === 0) break
|
15
|
+
acc = pair[0]
|
16
|
+
if (pair.length === 1) break
|
17
|
+
seed = pair[1]
|
18
|
+
}
|
19
|
+
return acc
|
20
|
+
}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import { fold } from './fold.js'
|
2
|
+
|
3
|
+
// addWithMaxOf10 function in functional program
|
4
|
+
const loop = (acc, seed) => {
|
5
|
+
if (seed.length > 0) {
|
6
|
+
let nextAcc = acc + seed[0]
|
7
|
+
let nextSeed = seed.slice(1)
|
8
|
+
return nextAcc > 10 ?
|
9
|
+
acc // * difference with iterator.
|
10
|
+
: loop(nextAcc, nextSeed) // *
|
11
|
+
}
|
12
|
+
return null
|
13
|
+
}
|
14
|
+
|
15
|
+
describe('fold', function () {
|
16
|
+
let source = [1, 2, 3, 4, 5, 6, 7]
|
17
|
+
|
18
|
+
it('before last', function () {
|
19
|
+
let fn = function (acc, seed) {
|
20
|
+
if (seed.length > 0) {
|
21
|
+
let nextAcc = acc + seed[0]
|
22
|
+
let nextSeed = seed.slice(1)
|
23
|
+
return nextAcc > 10
|
24
|
+
? [] // last acc is `acc` to return caller
|
25
|
+
: [nextAcc, nextSeed]
|
26
|
+
}
|
27
|
+
return null
|
28
|
+
};
|
29
|
+
|
30
|
+
let y = fold(fn, 0, source)
|
31
|
+
expect(y).toEqual(10)
|
32
|
+
});
|
33
|
+
|
34
|
+
it('include last', function () {
|
35
|
+
let fn = function (acc, seed) {
|
36
|
+
if (seed.length > 0) {
|
37
|
+
let nextAcc = acc + seed[0]
|
38
|
+
let nextSeed = seed.slice(1)
|
39
|
+
return nextAcc > 10
|
40
|
+
? [nextAcc] // last acc is `nextAcc` to return caller
|
41
|
+
: [nextAcc, nextSeed]
|
42
|
+
}
|
43
|
+
return null
|
44
|
+
};
|
45
|
+
|
46
|
+
let y = fold(fn, 0, source)
|
47
|
+
expect(y).toEqual(15)
|
48
|
+
|
49
|
+
});
|
50
|
+
|
51
|
+
});
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/**
|
2
|
+
* 数据优先的管道处理工具(从左向右执行)。
|
3
|
+
* 适合一次性数据处理流程。
|
4
|
+
* @public
|
5
|
+
* @param initialValue - 初始值。
|
6
|
+
* @param fns - 要应用的函数管道(从左向右执行)。
|
7
|
+
* @returns 经过管道处理后的结果。
|
8
|
+
* @throws {TypeError} 当参数包含非函数时抛出错误。
|
9
|
+
*/
|
10
|
+
export function pipe(
|
11
|
+
initialValue: any,
|
12
|
+
...fns: Array<(arg: any) => any>
|
13
|
+
): any;
|