@mic-rexjs/usecases-react 2.3.4 → 2.3.6

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.
@@ -63,6 +63,9 @@ const mathUseCase = (options = {}) => {
63
63
  };
64
64
  return { add, subtraction: subtractionReducer };
65
65
  };
66
+ const fieldPathUseCase = () => {
67
+ return objectUseCase();
68
+ };
66
69
  const Child = ({ textPrefix = '', onUpdate, onPathChange, onUndefinedEntity }) => {
67
70
  const [file, { setPath }] = useUseCase(fileUseCase);
68
71
  const { path, ext } = file || {};
@@ -558,6 +561,475 @@ describe('useUseCase', () => {
558
561
  expect(onPathChange2).toHaveBeenCalledTimes(1);
559
562
  expect(onPathChange2).toHaveBeenLastCalledWith(PATH_2, PATH_1);
560
563
  });
564
+ test('`options.watch` should be trigger after entity changed', () => {
565
+ const onPathChange = jest.fn();
566
+ const onSizeChange = jest.fn();
567
+ const { result } = renderHook(() => {
568
+ return useUseCase(defaultFile, fileUseCase, {
569
+ watch: {
570
+ path: onPathChange,
571
+ size: onSizeChange,
572
+ },
573
+ });
574
+ });
575
+ const { current: cores } = result;
576
+ const [, { setPath }] = cores;
577
+ expect(onPathChange).toHaveBeenCalledTimes(0);
578
+ act(() => {
579
+ setPath(PATH_1);
580
+ });
581
+ expect(onPathChange).toHaveBeenCalledTimes(1);
582
+ expect(onSizeChange).toHaveBeenCalledTimes(0);
583
+ expect(onPathChange).toHaveBeenLastCalledWith({
584
+ fieldPaths: ['path'],
585
+ newEntity: { ...defaultFile, path: PATH_1, ext: EXT_1 },
586
+ oldEntity: defaultFile,
587
+ newValue: PATH_1,
588
+ oldValue: '',
589
+ });
590
+ });
591
+ test('`options.watch` should trigger the latest one', () => {
592
+ const onPathChange1 = jest.fn();
593
+ const onPathChange2 = jest.fn();
594
+ let onPathChange = onPathChange1;
595
+ const { result } = renderHook(() => {
596
+ return useUseCase(defaultFile, fileUseCase, {
597
+ watch: {
598
+ path: onPathChange,
599
+ },
600
+ });
601
+ });
602
+ const { current: cores } = result;
603
+ const [, { setPath, setEntity }] = cores;
604
+ expect(onPathChange1).toHaveBeenCalledTimes(0);
605
+ act(() => {
606
+ setPath(PATH_1);
607
+ setEntity({ size: 5000 });
608
+ onPathChange = onPathChange2;
609
+ });
610
+ expect(onPathChange1).toHaveBeenCalledTimes(1);
611
+ expect(onPathChange1).toHaveBeenLastCalledWith({
612
+ fieldPaths: ['path'],
613
+ newEntity: { ...defaultFile, path: PATH_1, ext: EXT_1 },
614
+ oldEntity: defaultFile,
615
+ newValue: PATH_1,
616
+ oldValue: '',
617
+ });
618
+ act(() => {
619
+ setPath('');
620
+ });
621
+ expect(onPathChange1).toHaveBeenCalledTimes(1);
622
+ expect(onPathChange2).toHaveBeenCalledTimes(1);
623
+ expect(onPathChange2).toHaveBeenLastCalledWith({
624
+ fieldPaths: ['path'],
625
+ newEntity: { ...defaultFile, size: 5000 },
626
+ oldEntity: { ...defaultFile, path: PATH_1, ext: EXT_1, size: 5000 },
627
+ newValue: '',
628
+ oldValue: PATH_1,
629
+ });
630
+ });
631
+ test('`options.watch` should work with array field path of array', () => {
632
+ const onExtChange = jest.fn();
633
+ const onSizeChange = jest.fn();
634
+ const { result } = renderHook(() => {
635
+ return useUseCase({}, fieldPathUseCase, {
636
+ watch: {
637
+ 'list.ext': onExtChange,
638
+ 'list.size': onSizeChange,
639
+ },
640
+ });
641
+ });
642
+ const { current: cores } = result;
643
+ const [, { setEntity }] = cores;
644
+ act(() => {
645
+ setEntity({ list: [defaultFile] });
646
+ });
647
+ expect(onExtChange).toHaveBeenCalledTimes(1);
648
+ expect(onSizeChange).toHaveBeenCalledTimes(1);
649
+ expect(onExtChange).toHaveBeenCalledWith({
650
+ fieldPaths: ['list', '0', 'ext'],
651
+ newEntity: { list: [defaultFile] },
652
+ oldEntity: {},
653
+ newValue: '',
654
+ oldValue: void 0,
655
+ });
656
+ expect(onSizeChange).toHaveBeenCalledWith({
657
+ fieldPaths: ['list', '0', 'size'],
658
+ newEntity: { list: [defaultFile] },
659
+ oldEntity: {},
660
+ newValue: 0,
661
+ oldValue: void 0,
662
+ });
663
+ act(() => {
664
+ setEntity({ list: [{ ...defaultFile, ext: EXT_1 }] });
665
+ });
666
+ expect(onExtChange).toHaveBeenCalledTimes(2);
667
+ expect(onSizeChange).toHaveBeenCalledTimes(1);
668
+ expect(onExtChange).toHaveBeenCalledWith({
669
+ fieldPaths: ['list', '0', 'ext'],
670
+ newEntity: { list: [{ ...defaultFile, ext: EXT_1 }] },
671
+ oldEntity: { list: [defaultFile] },
672
+ newValue: EXT_1,
673
+ oldValue: '',
674
+ });
675
+ act(() => {
676
+ setEntity({
677
+ list: [
678
+ { ...defaultFile, ext: EXT_1 },
679
+ { ...defaultFile, ext: EXT_2 },
680
+ ],
681
+ });
682
+ });
683
+ expect(onExtChange).toHaveBeenCalledTimes(3);
684
+ expect(onSizeChange).toHaveBeenCalledTimes(2);
685
+ expect(onExtChange).toHaveBeenCalledWith({
686
+ fieldPaths: ['list', '1', 'ext'],
687
+ newEntity: {
688
+ list: [
689
+ { ...defaultFile, ext: EXT_1 },
690
+ { ...defaultFile, ext: EXT_2 },
691
+ ],
692
+ },
693
+ oldEntity: { list: [{ ...defaultFile, ext: EXT_1 }] },
694
+ newValue: EXT_2,
695
+ oldValue: void 0,
696
+ });
697
+ expect(onSizeChange).toHaveBeenCalledWith({
698
+ fieldPaths: ['list', '1', 'size'],
699
+ newEntity: {
700
+ list: [
701
+ { ...defaultFile, ext: EXT_1 },
702
+ { ...defaultFile, ext: EXT_2 },
703
+ ],
704
+ },
705
+ oldEntity: { list: [{ ...defaultFile, ext: EXT_1 }] },
706
+ newValue: 0,
707
+ oldValue: void 0,
708
+ });
709
+ act(() => {
710
+ setEntity({
711
+ nestedList: [],
712
+ });
713
+ });
714
+ expect(onExtChange).toHaveBeenCalledTimes(3);
715
+ expect(onSizeChange).toHaveBeenCalledTimes(2);
716
+ act(() => {
717
+ setEntity(() => {
718
+ return {
719
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
720
+ };
721
+ });
722
+ });
723
+ expect(onExtChange).toHaveBeenCalledTimes(6);
724
+ expect(onSizeChange).toHaveBeenCalledTimes(3);
725
+ expect(onExtChange).toHaveBeenNthCalledWith(4, {
726
+ fieldPaths: ['list', '0', 'ext'],
727
+ newEntity: {
728
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
729
+ },
730
+ oldEntity: {
731
+ list: [
732
+ { ...defaultFile, ext: EXT_1 },
733
+ { ...defaultFile, ext: EXT_2 },
734
+ ],
735
+ nestedList: [],
736
+ },
737
+ newValue: EXT_2,
738
+ oldValue: EXT_1,
739
+ });
740
+ expect(onExtChange).toHaveBeenNthCalledWith(5, {
741
+ fieldPaths: ['list', '1', 'ext'],
742
+ newEntity: {
743
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
744
+ },
745
+ oldEntity: {
746
+ list: [
747
+ { ...defaultFile, ext: EXT_1 },
748
+ { ...defaultFile, ext: EXT_2 },
749
+ ],
750
+ nestedList: [],
751
+ },
752
+ newValue: EXT_1,
753
+ oldValue: EXT_2,
754
+ });
755
+ expect(onExtChange).toHaveBeenNthCalledWith(6, {
756
+ fieldPaths: ['list', '2', 'ext'],
757
+ newEntity: {
758
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
759
+ },
760
+ oldEntity: {
761
+ list: [
762
+ { ...defaultFile, ext: EXT_1 },
763
+ { ...defaultFile, ext: EXT_2 },
764
+ ],
765
+ nestedList: [],
766
+ },
767
+ newValue: '',
768
+ oldValue: void 0,
769
+ });
770
+ expect(onSizeChange).toHaveBeenCalledWith({
771
+ fieldPaths: ['list', '2', 'size'],
772
+ newEntity: {
773
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
774
+ },
775
+ oldEntity: {
776
+ list: [
777
+ { ...defaultFile, ext: EXT_1 },
778
+ { ...defaultFile, ext: EXT_2 },
779
+ ],
780
+ nestedList: [],
781
+ },
782
+ newValue: 0,
783
+ oldValue: void 0,
784
+ });
785
+ act(() => {
786
+ setEntity(() => {
787
+ return {};
788
+ });
789
+ });
790
+ expect(onExtChange).toHaveBeenCalledTimes(9);
791
+ expect(onSizeChange).toHaveBeenCalledTimes(6);
792
+ expect(onExtChange).toHaveBeenNthCalledWith(7, {
793
+ fieldPaths: ['list', '0', 'ext'],
794
+ newEntity: {},
795
+ oldEntity: {
796
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
797
+ },
798
+ newValue: void 0,
799
+ oldValue: EXT_2,
800
+ });
801
+ expect(onExtChange).toHaveBeenNthCalledWith(8, {
802
+ fieldPaths: ['list', '1', 'ext'],
803
+ newEntity: {},
804
+ oldEntity: {
805
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
806
+ },
807
+ newValue: void 0,
808
+ oldValue: EXT_1,
809
+ });
810
+ expect(onExtChange).toHaveBeenNthCalledWith(9, {
811
+ fieldPaths: ['list', '2', 'ext'],
812
+ newEntity: {},
813
+ oldEntity: {
814
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
815
+ },
816
+ newValue: void 0,
817
+ oldValue: '',
818
+ });
819
+ expect(onSizeChange).toHaveBeenNthCalledWith(4, {
820
+ fieldPaths: ['list', '0', 'size'],
821
+ newEntity: {},
822
+ oldEntity: {
823
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
824
+ },
825
+ newValue: void 0,
826
+ oldValue: 0,
827
+ });
828
+ expect(onSizeChange).toHaveBeenNthCalledWith(5, {
829
+ fieldPaths: ['list', '1', 'size'],
830
+ newEntity: {},
831
+ oldEntity: {
832
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
833
+ },
834
+ newValue: void 0,
835
+ oldValue: 0,
836
+ });
837
+ expect(onSizeChange).toHaveBeenNthCalledWith(6, {
838
+ fieldPaths: ['list', '2', 'size'],
839
+ newEntity: {},
840
+ oldEntity: {
841
+ list: [{ ...defaultFile, ext: EXT_2 }, { ...defaultFile, ext: EXT_1 }, defaultFile],
842
+ },
843
+ newValue: void 0,
844
+ oldValue: 0,
845
+ });
846
+ });
847
+ test('`options.watch` should work with field path of array length', () => {
848
+ const onEntityLengthChange = jest.fn();
849
+ const onListLengthChange = jest.fn();
850
+ const lengthUseCase = () => {
851
+ return entityUseCase();
852
+ };
853
+ const { result } = renderHook(() => {
854
+ return useUseCase([], lengthUseCase, {
855
+ watch: {
856
+ length: onEntityLengthChange,
857
+ 'list.length': onListLengthChange,
858
+ },
859
+ });
860
+ });
861
+ const { current: cores } = result;
862
+ const [, { setEntity }] = cores;
863
+ expect(onEntityLengthChange).toHaveBeenCalledTimes(0);
864
+ expect(onListLengthChange).toHaveBeenCalledTimes(0);
865
+ act(() => {
866
+ setEntity([{ list: [defaultFile, defaultFile] }]);
867
+ });
868
+ expect(onEntityLengthChange).toHaveBeenCalledTimes(1);
869
+ expect(onListLengthChange).toHaveBeenCalledTimes(1);
870
+ expect(onEntityLengthChange).toHaveBeenCalledWith({
871
+ fieldPaths: ['length'],
872
+ newEntity: [{ list: [defaultFile, defaultFile] }],
873
+ oldEntity: [],
874
+ newValue: 1,
875
+ oldValue: 0,
876
+ });
877
+ expect(onListLengthChange).toHaveBeenCalledWith({
878
+ fieldPaths: ['0', 'list', 'length'],
879
+ newEntity: [{ list: [defaultFile, defaultFile] }],
880
+ oldEntity: [],
881
+ newValue: 2,
882
+ oldValue: void 0,
883
+ });
884
+ });
885
+ test('`options.watch` should work with field path of nested array', () => {
886
+ const onChange = jest.fn();
887
+ const { result } = renderHook(() => {
888
+ return useUseCase({}, fieldPathUseCase, {
889
+ watch: {
890
+ 'nestedList.size': onChange,
891
+ },
892
+ });
893
+ });
894
+ const { current: cores } = result;
895
+ const [, { setEntity }] = cores;
896
+ expect(onChange).toHaveBeenCalledTimes(0);
897
+ act(() => {
898
+ setEntity({ nestedList: [[[defaultFile]]] });
899
+ });
900
+ expect(onChange).toHaveBeenCalledTimes(1);
901
+ expect(onChange).toHaveBeenCalledWith({
902
+ fieldPaths: ['nestedList', '0', '0', '0', 'size'],
903
+ newEntity: { nestedList: [[[defaultFile]]] },
904
+ oldEntity: {},
905
+ newValue: 0,
906
+ oldValue: void 0,
907
+ });
908
+ act(() => {
909
+ setEntity({
910
+ nestedList: [
911
+ [
912
+ [
913
+ { ...defaultFile, size: 100 },
914
+ { ...defaultFile, size: 200 },
915
+ ],
916
+ ],
917
+ [[{ ...defaultFile, size: 300 }]],
918
+ ],
919
+ });
920
+ });
921
+ expect(onChange).toHaveBeenCalledTimes(4);
922
+ expect(onChange).toHaveBeenNthCalledWith(2, {
923
+ fieldPaths: ['nestedList', '0', '0', '0', 'size'],
924
+ newEntity: {
925
+ nestedList: [
926
+ [
927
+ [
928
+ { ...defaultFile, size: 100 },
929
+ { ...defaultFile, size: 200 },
930
+ ],
931
+ ],
932
+ [[{ ...defaultFile, size: 300 }]],
933
+ ],
934
+ },
935
+ oldEntity: { nestedList: [[[defaultFile]]] },
936
+ newValue: 100,
937
+ oldValue: 0,
938
+ });
939
+ expect(onChange).toHaveBeenNthCalledWith(3, {
940
+ fieldPaths: ['nestedList', '0', '0', '1', 'size'],
941
+ newEntity: {
942
+ nestedList: [
943
+ [
944
+ [
945
+ { ...defaultFile, size: 100 },
946
+ { ...defaultFile, size: 200 },
947
+ ],
948
+ ],
949
+ [[{ ...defaultFile, size: 300 }]],
950
+ ],
951
+ },
952
+ oldEntity: { nestedList: [[[defaultFile]]] },
953
+ newValue: 200,
954
+ oldValue: void 0,
955
+ });
956
+ expect(onChange).toHaveBeenNthCalledWith(4, {
957
+ fieldPaths: ['nestedList', '1', '0', '0', 'size'],
958
+ newEntity: {
959
+ nestedList: [
960
+ [
961
+ [
962
+ { ...defaultFile, size: 100 },
963
+ { ...defaultFile, size: 200 },
964
+ ],
965
+ ],
966
+ [[{ ...defaultFile, size: 300 }]],
967
+ ],
968
+ },
969
+ oldEntity: { nestedList: [[[defaultFile]]] },
970
+ newValue: 300,
971
+ oldValue: void 0,
972
+ });
973
+ });
974
+ test('`options.watch` should work with field path of object', () => {
975
+ const onExtChange = jest.fn();
976
+ const onSizeChange = jest.fn();
977
+ const { result } = renderHook(() => {
978
+ return useUseCase({}, fieldPathUseCase, {
979
+ watch: {
980
+ 'obj.file.ext': onExtChange,
981
+ 'obj.file.size': onSizeChange,
982
+ },
983
+ });
984
+ });
985
+ const { current: cores } = result;
986
+ const [, { setEntity }] = cores;
987
+ expect(onExtChange).toHaveBeenCalledTimes(0);
988
+ act(() => {
989
+ setEntity({
990
+ obj: {
991
+ file: {
992
+ ext: EXT_1,
993
+ },
994
+ },
995
+ });
996
+ });
997
+ expect(onExtChange).toHaveBeenCalledTimes(1);
998
+ expect(onSizeChange).toHaveBeenCalledTimes(0);
999
+ expect(onExtChange).toHaveBeenCalledWith({
1000
+ fieldPaths: ['obj', 'file', 'ext'],
1001
+ newEntity: {
1002
+ obj: {
1003
+ file: {
1004
+ ext: EXT_1,
1005
+ },
1006
+ },
1007
+ },
1008
+ oldEntity: {},
1009
+ newValue: EXT_1,
1010
+ oldValue: void 0,
1011
+ });
1012
+ act(() => {
1013
+ setEntity(() => {
1014
+ return {};
1015
+ });
1016
+ });
1017
+ expect(onExtChange).toHaveBeenCalledTimes(2);
1018
+ expect(onSizeChange).toHaveBeenCalledTimes(0);
1019
+ expect(onExtChange).toHaveBeenCalledWith({
1020
+ fieldPaths: ['obj', 'file', 'ext'],
1021
+ newEntity: {},
1022
+ oldEntity: {
1023
+ obj: {
1024
+ file: {
1025
+ ext: EXT_1,
1026
+ },
1027
+ },
1028
+ },
1029
+ newValue: void 0,
1030
+ oldValue: EXT_1,
1031
+ });
1032
+ });
561
1033
  test('multiple `Provider` should work', () => {
562
1034
  const onNumberMount = jest.fn();
563
1035
  const onStringMount = jest.fn();
@@ -879,6 +1351,130 @@ describe('useUseCase', () => {
879
1351
  expect(onSecondChange).toHaveBeenCalledTimes(1);
880
1352
  expect(onSecondChange).toHaveBeenLastCalledWith(defaultFile, { ...defaultFile, ext: EXT_1, path: PATH_1 });
881
1353
  });
1354
+ test('`options.watch` should be trigger at child elements', () => {
1355
+ const onExtChange = jest.fn();
1356
+ const firstButtonText = 'first button';
1357
+ const A = () => {
1358
+ const [, { setPath }] = useUseCase(fileUseCase, {
1359
+ watch: {
1360
+ ext: onExtChange,
1361
+ },
1362
+ });
1363
+ const onFirstClick = () => {
1364
+ setPath(PATH_1);
1365
+ };
1366
+ return _jsx("button", { onClick: onFirstClick, children: firstButtonText });
1367
+ };
1368
+ const B = () => {
1369
+ const [, , Provider] = useUseCase(defaultFile, fileUseCase, {});
1370
+ return (_jsx(Provider, { children: _jsx(A, {}) }));
1371
+ };
1372
+ render(_jsx(B, {}));
1373
+ expect(onExtChange).toHaveBeenCalledTimes(0);
1374
+ fireEvent.click(screen.getByText(firstButtonText));
1375
+ expect(onExtChange).toHaveBeenCalledTimes(1);
1376
+ expect(onExtChange).toHaveBeenLastCalledWith({
1377
+ fieldPaths: ['ext'],
1378
+ newEntity: { ...defaultFile, ext: EXT_1, path: PATH_1 },
1379
+ oldEntity: defaultFile,
1380
+ newValue: EXT_1,
1381
+ oldValue: '',
1382
+ });
1383
+ });
1384
+ test('`options.watch` should be trigger in orders', () => {
1385
+ let changeTimes = 0;
1386
+ const updateChangeTimes = (e) => {
1387
+ void e;
1388
+ return ++changeTimes;
1389
+ };
1390
+ const onChangeA = jest.fn(updateChangeTimes);
1391
+ const onChangeB = jest.fn(updateChangeTimes);
1392
+ const onChangeC = jest.fn(updateChangeTimes);
1393
+ const buttonText = 'my button';
1394
+ const A = ({ children }) => {
1395
+ useUseCase(fileUseCase, {
1396
+ watch: {
1397
+ ext: onChangeA,
1398
+ },
1399
+ });
1400
+ return _jsx(Fragment, { children: children });
1401
+ };
1402
+ const B = () => {
1403
+ useUseCase(fileUseCase, {
1404
+ watch: {
1405
+ ext: onChangeB,
1406
+ },
1407
+ });
1408
+ return _jsx(Fragment, {});
1409
+ };
1410
+ const C = () => {
1411
+ const [, { setPath }, Provider] = useUseCase(defaultFile, fileUseCase, {
1412
+ watch: {
1413
+ ext: onChangeC,
1414
+ },
1415
+ });
1416
+ const onFirstClick = () => {
1417
+ setPath(PATH_1);
1418
+ };
1419
+ return (_jsxs(Provider, { children: [_jsx(A, { children: _jsx(B, {}) }), _jsx("button", { onClick: onFirstClick, children: buttonText })] }));
1420
+ };
1421
+ render(_jsx(C, {}));
1422
+ expect(changeTimes).toBe(0);
1423
+ fireEvent.click(screen.getByText(buttonText));
1424
+ expect(onChangeA).toHaveReturnedWith(2);
1425
+ expect(onChangeB).toHaveReturnedWith(1);
1426
+ expect(onChangeC).toHaveReturnedWith(3);
1427
+ });
1428
+ test.each([UseCaseModes.Normal, UseCaseModes.Stateless])('[mode=%s]: `options.watch` should trigger the latest one in child components', (mode) => {
1429
+ const onFirstExtChange = jest.fn();
1430
+ const onSecondExtChange = jest.fn();
1431
+ let onExtChange = onFirstExtChange;
1432
+ const firstButtonText = 'first button';
1433
+ const secondButtonText = 'second button';
1434
+ const A = () => {
1435
+ const [, { setPath }] = useUseCase(fileUseCase, {
1436
+ watch: {
1437
+ ext: onExtChange,
1438
+ },
1439
+ });
1440
+ const onFirstClick = () => {
1441
+ setPath(PATH_1);
1442
+ onExtChange = onSecondExtChange;
1443
+ };
1444
+ const onSecondClick = () => {
1445
+ setPath('');
1446
+ };
1447
+ return (_jsxs(Fragment, { children: [_jsx("button", { onClick: onFirstClick, children: firstButtonText }), _jsx("button", { onClick: onSecondClick, children: secondButtonText })] }));
1448
+ };
1449
+ const B = () => {
1450
+ const [file, setFile] = useState(defaultFile);
1451
+ const [, , Provider] = useUseCase(file, fileUseCase, mode, {
1452
+ onChange(newFile) {
1453
+ if ((mode & UseCaseModes.Stateless) !== UseCaseModes.Stateless) {
1454
+ return;
1455
+ }
1456
+ // `stateless` 不会保存状态,所以需要手动保存
1457
+ setFile(newFile);
1458
+ },
1459
+ });
1460
+ return (_jsx(Provider, { children: _jsx(A, {}) }));
1461
+ };
1462
+ render(_jsx(B, {}));
1463
+ expect(onFirstExtChange).toHaveBeenCalledTimes(0);
1464
+ fireEvent.click(screen.getByText(firstButtonText));
1465
+ expect(onFirstExtChange).toHaveBeenCalledTimes(1);
1466
+ expect(onSecondExtChange).toHaveBeenCalledTimes(0);
1467
+ fireEvent.click(screen.getByText(secondButtonText));
1468
+ expect(onFirstExtChange).toHaveBeenCalledTimes(1);
1469
+ expect(onSecondExtChange).toHaveBeenCalledTimes(1);
1470
+ expect(onSecondExtChange).toHaveBeenLastCalledWith({
1471
+ fieldPaths: ['ext'],
1472
+ newEntity: defaultFile,
1473
+ oldEntity: { ...defaultFile, ext: EXT_1, path: PATH_1 },
1474
+ newValue: '',
1475
+ oldValue: EXT_1,
1476
+ });
1477
+ });
882
1478
  });
883
1479
  describe('`useUseCase` should work the same as `useReducers`', () => {
884
1480
  test('`entity` should equal `defaultFile`', () => {
@@ -19,12 +19,26 @@ export type CoreCollection<T, TEntityReducers extends EntityReducers<T>> = RootC
19
19
  export interface EntityGetter<T> {
20
20
  (): T;
21
21
  }
22
+ export interface EntityWatchEvent<T, TValue = unknown> {
23
+ fieldPaths: string[];
24
+ newEntity: T;
25
+ newValue: TValue;
26
+ oldEntity: T;
27
+ oldValue: TValue;
28
+ }
29
+ export interface EntityWatcher<T, TValue = unknown> {
30
+ (event: EntityWatchEvent<T, TValue>): void;
31
+ }
22
32
  export type PropertyPath<T, K extends keyof T = keyof T & string> = K extends infer TKey ? `${TKey & (string | number)}${'' | DotAccessorFieldPath<T[TKey & keyof T]>}` : never;
23
33
  export type DotAccessorFieldPath<T> = T extends unknown[] ? '.length' | `${'' | `.${number}`}${DotAccessorFieldPath<T[number]>}` : T extends object ? `.${PropertyPath<T>}` : '';
24
34
  export type FieldPath<T> = T extends unknown[] ? FieldPath<T[number]> | 'length' : T extends object ? PropertyPath<T> : never;
25
35
  export type ExtractPropertyType<T, TPath> = TPath extends `${infer TKey}.${infer TSubPath}` ? T extends unknown[] ? ExtractPropertyType<T[number], TKey extends `${number}` ? TSubPath : TPath> : ExtractPropertyType<T[TKey & keyof T], TSubPath> : T extends unknown[] ? TPath extends 'length' ? T[TPath] : TPath extends `${number}` ? T[number] : ExtractPropertyType<T[number], TPath> : T[TPath & keyof T];
26
36
  export type ExtractFieldType<T, TPath> = TPath extends `${infer TKey}.${infer TSubProperty}` ? ExtractPropertyType<T[TKey & keyof T], TSubProperty> : T[TPath & keyof T];
37
+ export type EntityWatchMap<T> = {
38
+ [K in FieldPath<T>]?: EntityWatcher<T, ExtractFieldType<T, K>>;
39
+ };
27
40
  export interface UseCaseHookContextualOptions<T> {
41
+ watch?: EntityWatchMap<T>;
28
42
  onChange?(newEntity: T, oldEntity: T): void;
29
43
  }
30
44
  export interface UseCaseHookOwnOptions<T, TUseCaseOptions extends object> extends UseCaseHookContextualOptions<T> {
package/es/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { UseCaseFragment } from './components/UseCaseFragment';
2
2
  export { useUseCase } from './hooks/useUseCase';
3
3
  export { useStatelessUseCase } from './hooks/useStatelessUseCase';
4
- export type { RootCoreCollection, ContextualCoreCollection, CoreCollection } from './hooks/useUseCase/types';
4
+ export type { EntityWatchEvent, RootCoreCollection, ContextualCoreCollection, CoreCollection, } from './hooks/useUseCase/types';
5
5
  export type { UseCaseHookOptions } from './hooks/useUseCase/types';
6
6
  export type { UseCaseProvider } from './hooks/useProvider/types';
7
7
  export type { UseCaseFragmentProps } from './components/UseCaseFragment/types';
@@ -1,6 +1,6 @@
1
1
  import { createEntityReducers } from '@mic-rexjs/usecases';
2
2
  export const initEntityReducers = (usecase, store, options) => {
3
- const { onChange, options: usecaseOptions, ...restUseCaseOptions } = options;
3
+ const { watch, onChange, options: usecaseOptions, ...restUseCaseOptions } = options;
4
4
  return createEntityReducers(store, usecase, {
5
5
  ...restUseCaseOptions,
6
6
  ...usecaseOptions,
@@ -0,0 +1,2 @@
1
+ import { EntityWatchMap } from '../../hooks/useUseCase/types';
2
+ export declare const triggerWatchers: <T>(watch: EntityWatchMap<T>, newEntity: T, oldEntity: T) => void;
@@ -0,0 +1,14 @@
1
+ import { compareProperty } from '../compareProperty';
2
+ export const triggerWatchers = (watch, newEntity, oldEntity) => {
3
+ for (const [fieldPath, watcher] of Object.entries(watch)) {
4
+ compareProperty(oldEntity, newEntity, fieldPath, (oldValue, newValue, fieldPaths) => {
5
+ watcher({
6
+ fieldPaths,
7
+ newEntity,
8
+ oldEntity,
9
+ newValue,
10
+ oldValue,
11
+ });
12
+ });
13
+ }
14
+ };