@neko-os/ui 0.0.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/NekoUI.js +1 -1
- package/dist/abstractions/FlatList.native.js +1 -1
- package/dist/abstractions/KeyboardAvoidingView.js +1 -0
- package/dist/abstractions/KeyboardAvoidingView.native.js +1 -0
- package/dist/abstractions/ScrollView.native.js +1 -1
- package/dist/components/actions/ActionsDrawer.js +1 -0
- package/dist/components/actions/Button.js +1 -1
- package/dist/components/actions/FloatingMenu.js +1 -0
- package/dist/components/actions/index.js +1 -1
- package/dist/components/animations/AnimatedTopBar.js +1 -0
- package/dist/components/animations/AnimatedTopBar.native.js +1 -0
- package/dist/components/animations/AnimatedTopBar.web.js +1 -0
- package/dist/components/animations/ParallaxHeader.js +1 -0
- package/dist/components/animations/ParallaxHeader.native.js +1 -0
- package/dist/components/animations/ParallaxHeader.web.js +1 -0
- package/dist/components/animations/ReanimatedScrollHandler.js +1 -0
- package/dist/components/animations/ReanimatedScrollHandler.native.js +1 -0
- package/dist/components/animations/ReanimatedScrollHandler.web.js +1 -0
- package/dist/components/animations/index.js +1 -1
- package/dist/components/feedback/alerter.js +1 -1
- package/dist/components/feedback/confirmer.js +1 -1
- package/dist/components/form/FormItem.js +1 -1
- package/dist/components/form/FormList.js +1 -1
- package/dist/components/form/SubmitButton.js +1 -1
- package/dist/components/form/index.js +1 -1
- package/dist/components/form/useNewForm.js +1 -1
- package/dist/components/form/validation/defaultMessages.js +1 -0
- package/dist/components/form/validation/index.js +1 -0
- package/dist/components/form/validation/normalizeRules.js +1 -0
- package/dist/components/form/validation/shouldValidateOn.js +1 -0
- package/dist/components/form/validation/validateRules.js +1 -0
- package/dist/components/form/validation/validators.js +1 -0
- package/dist/components/index.js +1 -1
- package/dist/components/inputs/InputWrapper.js +1 -1
- package/dist/components/inputs/NumberInput.js +1 -1
- package/dist/components/inputs/Picker.js +1 -1
- package/dist/components/inputs/Select.js +1 -1
- package/dist/components/modals/bottomDrawer/index.js +1 -0
- package/dist/components/modals/bottomDrawer/index.native.js +1 -0
- package/dist/components/modals/bottomDrawer/index.web.js +1 -0
- package/dist/components/modals/bottomDrawer/native/BottomDrawer.js +1 -0
- package/dist/components/modals/bottomDrawer/native/DrawerContext.js +1 -0
- package/dist/components/modals/bottomDrawer/native/DrawerHandle.js +1 -0
- package/dist/components/modals/bottomDrawer/native/DrawerScrollView.js +1 -0
- package/dist/components/modals/bottomDrawer/native/createDrawerScrollComponent.js +1 -0
- package/dist/components/modals/bottomDrawer/web/BottomDrawer.js +1 -0
- package/dist/components/modals/drawer/Drawer.js +1 -0
- package/dist/components/modals/index.js +1 -0
- package/dist/components/modals/modal/Modal.js +1 -0
- package/dist/components/modals/modal/Modal.native.js +1 -0
- package/dist/components/modals/modal/ModalBackdrop.js +1 -0
- package/dist/components/modals/modal/ModalContent.js +1 -0
- package/dist/components/modals/modal/ModalFooter.js +1 -0
- package/dist/components/modals/modal/ModalHeader.js +1 -0
- package/dist/components/modals/modal/handler/ModalsHandler.js +1 -0
- package/dist/components/modals/router/ModalRoute.js +1 -0
- package/dist/components/modals/router/ModalsRouter.js +1 -0
- package/dist/components/modals/router/ModalsRouterContext.js +1 -0
- package/dist/components/modals/router/index.js +1 -0
- package/dist/components/modals/router/useAllModalsParams.js +1 -0
- package/dist/components/modals/router/useModalParams.js +1 -0
- package/dist/components/modals/router/useModalsNavigation.js +1 -0
- package/dist/components/modals/router/useUpdateModalContainer.js +1 -0
- package/dist/components/presentation/Avatar.js +1 -1
- package/dist/components/presentation/AvatarLabel.js +1 -1
- package/dist/components/presentation/LabelValue.js +1 -1
- package/dist/components/presentation/Result.js +1 -1
- package/dist/components/presentation/Tooltip.js +1 -1
- package/dist/components/sections/Section.js +1 -0
- package/dist/components/sections/SectionItem.js +1 -0
- package/dist/components/sections/SectionItemDropdown.js +1 -0
- package/dist/components/sections/SectionItemLink.js +1 -0
- package/dist/components/sections/index.js +1 -0
- package/dist/components/state/StatePresenter.js +1 -0
- package/dist/components/state/index.js +1 -1
- package/dist/components/structure/BlurView.js +1 -1
- package/dist/components/structure/KeyboardAvoidingView.js +1 -0
- package/dist/components/structure/TopBar.js +1 -0
- package/dist/components/structure/index.js +1 -1
- package/dist/components/structure/popover/Popover.js +1 -1
- package/dist/components/structure/popover/Popover.native.js +1 -1
- package/dist/components/structure/popover/Popover_BU.js +1 -1
- package/dist/components/text/DateText.js +1 -0
- package/dist/components/text/index.js +1 -1
- package/dist/components/theme/ThemePicker.js +1 -1
- package/dist/components/theme/ThemePickerDrawer.js +1 -1
- package/dist/helpers/index.js +1 -1
- package/dist/helpers/storage.js +1 -1
- package/dist/responsive/responsiveHooks.js +1 -1
- package/dist/theme/ThemeHandler.js +1 -1
- package/dist/theme/default/base.js +1 -1
- package/dist/theme/default/blackTheme.js +1 -1
- package/dist/theme/default/cyberpunkTheme.js +1 -1
- package/dist/theme/default/darkTheme.js +1 -1
- package/dist/theme/default/hackerTheme.js +1 -1
- package/dist/theme/default/lightTheme.js +1 -1
- package/dist/theme/default/paperTheme.js +1 -1
- package/dist/theme/default/themes.js +1 -1
- package/package.json +1 -1
- package/src/NekoUI.js +1 -1
- package/src/abstractions/FlatList.native.js +2 -1
- package/src/abstractions/KeyboardAvoidingView.js +3 -0
- package/src/abstractions/KeyboardAvoidingView.native.js +3 -0
- package/src/abstractions/ScrollView.native.js +2 -2
- package/src/components/actions/ActionsDrawer.js +68 -0
- package/src/components/actions/Button.js +2 -1
- package/src/components/actions/FloatingMenu.js +39 -0
- package/src/components/actions/index.js +2 -0
- package/src/components/animations/AnimatedTopBar.js +10 -0
- package/src/components/animations/AnimatedTopBar.native.js +34 -0
- package/src/components/animations/AnimatedTopBar.web.js +1 -0
- package/src/components/animations/ParallaxHeader.js +9 -0
- package/src/components/animations/ParallaxHeader.native.js +32 -0
- package/src/components/animations/ParallaxHeader.web.js +32 -0
- package/src/components/animations/ReanimatedScrollHandler.js +8 -0
- package/src/components/animations/ReanimatedScrollHandler.native.js +24 -0
- package/src/components/animations/ReanimatedScrollHandler.web.js +1 -0
- package/src/components/animations/index.js +3 -0
- package/src/components/feedback/alerter.js +1 -1
- package/src/components/feedback/confirmer.js +1 -1
- package/src/components/form/FormItem.js +42 -5
- package/src/components/form/FormList.js +23 -4
- package/src/components/form/SubmitButton.js +4 -2
- package/src/components/form/index.js +1 -0
- package/src/components/form/useNewForm.js +108 -15
- package/src/components/form/validation/defaultMessages.js +20 -0
- package/src/components/form/validation/index.js +5 -0
- package/src/components/form/validation/normalizeRules.js +22 -0
- package/src/components/form/validation/shouldValidateOn.js +21 -0
- package/src/components/form/validation/validateRules.js +83 -0
- package/src/components/form/validation/validators.js +82 -0
- package/src/components/index.js +2 -0
- package/src/components/inputs/InputWrapper.js +1 -1
- package/src/components/inputs/NumberInput.js +6 -5
- package/src/components/inputs/Picker.js +3 -2
- package/src/components/inputs/Select.js +31 -15
- package/src/components/modals/bottomDrawer/index.js +3 -0
- package/src/components/{structure → modals}/bottomDrawer/index.native.js +2 -1
- package/src/components/{structure → modals}/bottomDrawer/index.web.js +2 -1
- package/src/components/{structure → modals}/bottomDrawer/native/BottomDrawer.js +15 -21
- package/src/components/{structure → modals}/bottomDrawer/native/DrawerHandle.js +1 -1
- package/src/components/modals/bottomDrawer/native/DrawerScrollView.js +5 -0
- package/src/components/modals/bottomDrawer/native/createDrawerScrollComponent.js +131 -0
- package/src/components/modals/index.js +4 -0
- package/src/components/{structure → modals}/modal/Modal.native.js +1 -1
- package/src/components/{structure → modals}/modal/ModalBackdrop.js +1 -1
- package/src/components/{structure → modals}/modal/ModalContent.js +1 -1
- package/src/components/{structure → modals}/modal/ModalFooter.js +1 -1
- package/src/components/{structure → modals}/modal/ModalHeader.js +1 -1
- package/src/components/modals/router/ModalRoute.js +15 -0
- package/src/components/modals/router/ModalsRouter.js +120 -0
- package/src/components/modals/router/ModalsRouterContext.js +16 -0
- package/src/components/modals/router/index.js +6 -0
- package/src/components/modals/router/useAllModalsParams.js +6 -0
- package/src/components/modals/router/useModalParams.js +6 -0
- package/src/components/modals/router/useModalsNavigation.js +6 -0
- package/src/components/modals/router/useUpdateModalContainer.js +6 -0
- package/src/components/presentation/Avatar.js +2 -2
- package/src/components/presentation/AvatarLabel.js +2 -0
- package/src/components/presentation/LabelValue.js +7 -5
- package/src/components/presentation/Result.js +2 -2
- package/src/components/presentation/Tooltip.js +1 -1
- package/src/components/sections/Section.js +50 -0
- package/src/components/sections/SectionItem.js +24 -0
- package/src/components/sections/SectionItemDropdown.js +68 -0
- package/src/components/sections/SectionItemLink.js +33 -0
- package/src/components/sections/index.js +4 -0
- package/src/components/state/StatePresenter.js +41 -0
- package/src/components/state/index.js +1 -0
- package/src/components/structure/BlurView.js +1 -0
- package/src/components/structure/KeyboardAvoidingView.js +52 -0
- package/src/components/structure/TopBar.js +45 -0
- package/src/components/structure/index.js +2 -3
- package/src/components/structure/popover/Popover.js +1 -1
- package/src/components/structure/popover/Popover.native.js +1 -1
- package/src/components/structure/popover/Popover_BU.js +1 -1
- package/src/components/text/DateText.js +11 -0
- package/src/components/text/index.js +1 -0
- package/src/components/theme/ThemePicker.js +1 -2
- package/src/components/theme/ThemePickerDrawer.js +3 -4
- package/src/helpers/index.js +1 -0
- package/src/helpers/storage.js +32 -9
- package/src/responsive/responsiveHooks.js +6 -0
- package/src/theme/ThemeHandler.js +6 -3
- package/src/theme/default/base.js +16 -4
- package/src/theme/default/blackTheme.js +33 -21
- package/src/theme/default/cyberpunkTheme.js +24 -22
- package/src/theme/default/darkTheme.js +1 -0
- package/src/theme/default/hackerTheme.js +40 -19
- package/src/theme/default/lightTheme.js +1 -0
- package/src/theme/default/paperTheme.js +14 -0
- package/src/theme/default/themes.js +0 -9
- package/dist/components/structure/bottomDrawer/index.js +0 -1
- package/dist/components/structure/bottomDrawer/index.native.js +0 -1
- package/dist/components/structure/bottomDrawer/index.web.js +0 -1
- package/dist/components/structure/bottomDrawer/native/BottomDrawer.js +0 -1
- package/dist/components/structure/bottomDrawer/native/DrawerContext.js +0 -1
- package/dist/components/structure/bottomDrawer/native/DrawerHandle.js +0 -1
- package/dist/components/structure/bottomDrawer/native/DrawerScrollView.js +0 -1
- package/dist/components/structure/bottomDrawer/web/BottomDrawer.js +0 -1
- package/dist/components/structure/drawer/Drawer.js +0 -1
- package/dist/components/structure/modal/Modal.js +0 -1
- package/dist/components/structure/modal/Modal.native.js +0 -1
- package/dist/components/structure/modal/ModalBackdrop.js +0 -1
- package/dist/components/structure/modal/ModalContent.js +0 -1
- package/dist/components/structure/modal/ModalFooter.js +0 -1
- package/dist/components/structure/modal/ModalHeader.js +0 -1
- package/dist/components/structure/modal/handler/ModalsHandler.js +0 -1
- package/dist/theme/default/deepWoodsTheme.js +0 -1
- package/dist/theme/default/forestTheme.js +0 -1
- package/dist/theme/default/midnightTheme.js +0 -1
- package/dist/theme/default/msdosTheme.js +0 -1
- package/dist/theme/default/oceanTheme.js +0 -1
- package/dist/theme/default/pastelTheme.js +0 -1
- package/dist/theme/default/sunsetTheme.js +0 -1
- package/src/components/structure/bottomDrawer/index.js +0 -1
- package/src/components/structure/bottomDrawer/native/DrawerScrollView.js +0 -83
- package/src/theme/default/deepWoodsTheme.js +0 -34
- package/src/theme/default/forestTheme.js +0 -34
- package/src/theme/default/midnightTheme.js +0 -34
- package/src/theme/default/msdosTheme.js +0 -55
- package/src/theme/default/oceanTheme.js +0 -34
- package/src/theme/default/pastelTheme.js +0 -34
- package/src/theme/default/sunsetTheme.js +0 -35
- /package/dist/components/{structure → modals}/bottomDrawer/native/utils.js +0 -0
- /package/dist/components/{structure → modals}/drawer/Drawer.native.js +0 -0
- /package/dist/components/{structure → modals}/drawer/Drawer.web.js +0 -0
- /package/dist/components/{structure → modals}/drawer/index.js +0 -0
- /package/dist/components/{structure → modals}/modal/index.js +0 -0
- /package/src/components/{structure → modals}/bottomDrawer/native/DrawerContext.js +0 -0
- /package/src/components/{structure → modals}/bottomDrawer/native/utils.js +0 -0
- /package/src/components/{structure → modals}/bottomDrawer/web/BottomDrawer.js +0 -0
- /package/src/components/{structure → modals}/drawer/Drawer.js +0 -0
- /package/src/components/{structure → modals}/drawer/Drawer.native.js +0 -0
- /package/src/components/{structure → modals}/drawer/Drawer.web.js +0 -0
- /package/src/components/{structure → modals}/drawer/index.js +0 -0
- /package/src/components/{structure → modals}/modal/Modal.js +0 -0
- /package/src/components/{structure → modals}/modal/handler/ModalsHandler.js +0 -0
- /package/src/components/{structure → modals}/modal/index.js +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Animated, { useAnimatedStyle, useSharedValue, useAnimatedReaction, withTiming } from 'react-native-reanimated'
|
|
2
|
+
|
|
3
|
+
import { TopBar } from '../structure'
|
|
4
|
+
import { useReanimatedScroll } from './ReanimatedScrollHandler'
|
|
5
|
+
import { useSafeAreaInsets } from '../../abstractions/helpers/useSafeAreaInsets'
|
|
6
|
+
|
|
7
|
+
export function AnimatedTopBar({ showAfter = 90, duration = 300, fade = true, slide, ...props }) {
|
|
8
|
+
const { scrollY } = useReanimatedScroll()
|
|
9
|
+
const { top: safeTop } = useSafeAreaInsets()
|
|
10
|
+
|
|
11
|
+
const visibility = useSharedValue(0)
|
|
12
|
+
|
|
13
|
+
useAnimatedReaction(
|
|
14
|
+
() => scrollY.value >= showAfter - safeTop,
|
|
15
|
+
(shouldShow, prev) => {
|
|
16
|
+
if (shouldShow !== prev) {
|
|
17
|
+
visibility.value = withTiming(shouldShow ? 1 : 0, { duration })
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
23
|
+
const style = {}
|
|
24
|
+
if (fade) style.opacity = visibility.value
|
|
25
|
+
if (slide) style.transform = [{ translateY: (1 - visibility.value) * (-50 - safeTop) }]
|
|
26
|
+
return style
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Animated.View style={[{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 90 }, animatedStyle]}>
|
|
31
|
+
<TopBar {...props} />
|
|
32
|
+
</Animated.View>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AnimatedTopBar } from './AnimatedTopBar.native'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
|
|
2
|
+
|
|
3
|
+
import { useReanimatedScroll } from './ReanimatedScrollHandler'
|
|
4
|
+
|
|
5
|
+
const SCALE_FACTOR = 2
|
|
6
|
+
|
|
7
|
+
export function ParallaxHeader({ children, height = 200, parallaxSpeed = 0.5, disableResistence }) {
|
|
8
|
+
const { scrollY } = useReanimatedScroll()
|
|
9
|
+
|
|
10
|
+
const imageStyle = useAnimatedStyle(() => {
|
|
11
|
+
const scale = scrollY.value < 0 ? 1 + (Math.abs(scrollY.value) / height) * SCALE_FACTOR : 1
|
|
12
|
+
const translateY =
|
|
13
|
+
scrollY.value < 0 ? 0 : interpolate(scrollY.value, [0, height], [0, height * parallaxSpeed], Extrapolation.CLAMP)
|
|
14
|
+
return { transform: [{ translateY }, { scale }] }
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const containerStyle = useAnimatedStyle(() => {
|
|
18
|
+
let calcHeight = height
|
|
19
|
+
if (disableResistence) calcHeight = scrollY.value < 0 ? height + Math.abs(scrollY.value) : height
|
|
20
|
+
return {
|
|
21
|
+
height: calcHeight,
|
|
22
|
+
overflow: scrollY.value < 0 ? 'visible' : 'hidden',
|
|
23
|
+
zIndex: -1,
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Animated.View style={containerStyle}>
|
|
29
|
+
<Animated.View style={imageStyle}>{children}</Animated.View>
|
|
30
|
+
</Animated.View>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
|
|
2
|
+
|
|
3
|
+
import { useReanimatedScroll } from './ReanimatedScrollHandler'
|
|
4
|
+
|
|
5
|
+
const SCALE_FACTOR = 2
|
|
6
|
+
|
|
7
|
+
export function ParallaxHeader({ children, height = 200, parallaxSpeed = 0.5, disableResistence }) {
|
|
8
|
+
const { scrollY } = useReanimatedScroll()
|
|
9
|
+
|
|
10
|
+
const imageStyle = useAnimatedStyle(() => {
|
|
11
|
+
const scale = scrollY.value < 0 ? 1 + (Math.abs(scrollY.value) / height) * SCALE_FACTOR : 1
|
|
12
|
+
const translateY =
|
|
13
|
+
scrollY.value < 0 ? 0 : interpolate(scrollY.value, [0, height], [0, height * parallaxSpeed], Extrapolation.CLAMP)
|
|
14
|
+
return { transform: [{ translateY }, { scale }] }
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const containerStyle = useAnimatedStyle(() => {
|
|
18
|
+
let calcHeight = height
|
|
19
|
+
if (disableResistence) calcHeight = scrollY.value < 0 ? height + Math.abs(scrollY.value) : height
|
|
20
|
+
return {
|
|
21
|
+
height: calcHeight,
|
|
22
|
+
overflow: scrollY.value < 0 ? 'visible' : 'hidden',
|
|
23
|
+
zIndex: -1,
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Animated.View style={containerStyle}>
|
|
29
|
+
<Animated.View style={imageStyle}>{children}</Animated.View>
|
|
30
|
+
</Animated.View>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from 'react'
|
|
2
|
+
import { useSharedValue, useAnimatedScrollHandler } from 'react-native-reanimated'
|
|
3
|
+
|
|
4
|
+
const ReanimatedScrollContext = createContext(null)
|
|
5
|
+
|
|
6
|
+
export function useReanimatedScroll() {
|
|
7
|
+
const context = useContext(ReanimatedScrollContext)
|
|
8
|
+
if (!context) throw new Error('useReanimatedScroll must be used within ReanimatedScrollHandler')
|
|
9
|
+
return context
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ReanimatedScrollHandler({ children }) {
|
|
13
|
+
const scrollY = useSharedValue(0)
|
|
14
|
+
|
|
15
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
16
|
+
onScroll: (event) => {
|
|
17
|
+
scrollY.value = event.contentOffset.y
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const value = useMemo(() => ({ scrollY, scrollHandler }), [])
|
|
22
|
+
|
|
23
|
+
return <ReanimatedScrollContext.Provider value={value}>{children}</ReanimatedScrollContext.Provider>
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ReanimatedScrollHandler, useReanimatedScroll } from './ReanimatedScrollHandler.native'
|
|
@@ -2,7 +2,7 @@ import { is } from 'ramda'
|
|
|
2
2
|
|
|
3
3
|
import { Button } from '../actions'
|
|
4
4
|
import { Result } from '../presentation'
|
|
5
|
-
import { useModalOpener } from '../
|
|
5
|
+
import { useModalOpener } from '../modals/modal'
|
|
6
6
|
|
|
7
7
|
export function useAlerter() {
|
|
8
8
|
const { open } = useModalOpener()
|
|
@@ -5,7 +5,7 @@ import { Button } from '../actions'
|
|
|
5
5
|
import { RESULT_TYPES } from '../presentation/Result'
|
|
6
6
|
import { Result } from '../presentation'
|
|
7
7
|
import { View } from '../structure/View'
|
|
8
|
-
import { useModalOpener } from '../
|
|
8
|
+
import { useModalOpener } from '../modals/modal'
|
|
9
9
|
|
|
10
10
|
function Footer({ cancelLabel, confirmLabel, onConfirm, type, onClose }) {
|
|
11
11
|
const [loading, setLoading] = React.useState(false)
|
|
@@ -5,38 +5,75 @@ import { Text } from '../text/Text'
|
|
|
5
5
|
import { View } from '../structure/View'
|
|
6
6
|
import { clearProps } from '../../modifiers/_helpers'
|
|
7
7
|
import { useFormInstance, useFormState } from './Form'
|
|
8
|
+
import { shouldValidateOn } from './validation'
|
|
8
9
|
|
|
9
|
-
export function FormItem({
|
|
10
|
+
export function FormItem({
|
|
11
|
+
name,
|
|
12
|
+
label,
|
|
13
|
+
isAbsolutePath,
|
|
14
|
+
children,
|
|
15
|
+
useDefaultValue,
|
|
16
|
+
rules,
|
|
17
|
+
validateTrigger = 'onSubmit',
|
|
18
|
+
...props
|
|
19
|
+
}) {
|
|
10
20
|
const form = useFormInstance()
|
|
11
21
|
const formState = useFormState()
|
|
12
22
|
const listPath = useRelativePath(name, { isAbsolutePath })
|
|
23
|
+
const listPathStr = listPath.join('$NEKOJOIN$')
|
|
13
24
|
const [value, setValue] = React.useState(form.getFieldValue(listPath))
|
|
14
|
-
const error = form.getError(listPath)
|
|
25
|
+
const [error, setError] = React.useState(form.getError(listPath))
|
|
15
26
|
|
|
27
|
+
// Register rules with the form
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
return form.registerRules(listPath, rules, validateTrigger)
|
|
30
|
+
}, [listPathStr, JSON.stringify(rules), validateTrigger])
|
|
31
|
+
|
|
32
|
+
// Listen for value changes
|
|
16
33
|
React.useEffect(() => {
|
|
17
34
|
return form.registerListener(listPath, (val) => setValue(val))
|
|
18
|
-
}, [
|
|
35
|
+
}, [listPathStr])
|
|
36
|
+
|
|
37
|
+
// Listen for error changes
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
return form.registerErrorListener(listPath, (err) => setError(err))
|
|
40
|
+
}, [listPathStr])
|
|
19
41
|
|
|
20
42
|
const handleChange = (e) => {
|
|
21
43
|
const val = e?.target?.value ?? e
|
|
22
44
|
form.setFieldValue(listPath, val)
|
|
45
|
+
|
|
46
|
+
if (shouldValidateOn('onChange', rules, validateTrigger)) {
|
|
47
|
+
form.validateField(listPath, 'onChange')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleBlur = (e, originalOnBlur) => {
|
|
52
|
+
if (originalOnBlur) originalOnBlur(e)
|
|
53
|
+
|
|
54
|
+
if (shouldValidateOn('onBlur', rules, validateTrigger)) {
|
|
55
|
+
form.validateField(listPath, 'onBlur')
|
|
56
|
+
}
|
|
23
57
|
}
|
|
24
58
|
|
|
25
59
|
let valueKey = 'value'
|
|
26
60
|
if (!!useDefaultValue) valueKey = 'defaultValue'
|
|
27
61
|
|
|
62
|
+
const child = typeof children === 'function' ? null : React.Children.only(children)
|
|
63
|
+
const originalOnBlur = child?.props?.onBlur
|
|
64
|
+
|
|
28
65
|
const childProps = clearProps({
|
|
29
66
|
[valueKey]: value === undefined ? '' : value,
|
|
30
67
|
onChange: handleChange,
|
|
31
|
-
|
|
68
|
+
onBlur: (e) => handleBlur(e, originalOnBlur),
|
|
32
69
|
disabled: formState?.disabled === true || undefined,
|
|
70
|
+
error: !!error || undefined,
|
|
33
71
|
})
|
|
34
72
|
|
|
35
73
|
let content
|
|
36
74
|
if (typeof children === 'function') {
|
|
37
75
|
content = children(childProps)
|
|
38
76
|
} else {
|
|
39
|
-
const child = React.Children.only(children)
|
|
40
77
|
content = React.cloneElement(child, { ...child.props, ...childProps })
|
|
41
78
|
}
|
|
42
79
|
|
|
@@ -3,22 +3,32 @@ import React from 'react'
|
|
|
3
3
|
import { FormGroup, useRelativePath } from './FormGroup'
|
|
4
4
|
import { Text } from '../text/Text'
|
|
5
5
|
import { useFormInstance } from './Form'
|
|
6
|
+
import { shouldValidateOn } from './validation'
|
|
6
7
|
|
|
7
8
|
const FormListContext = React.createContext(null)
|
|
8
9
|
const useFormList = () => React.useContext(FormListContext)
|
|
9
10
|
|
|
10
|
-
export function FormList({ name, isAbsolutePath, children }) {
|
|
11
|
+
export function FormList({ name, isAbsolutePath, children, rules, validateTrigger = 'onSubmit' }) {
|
|
11
12
|
const form = useFormInstance()
|
|
12
13
|
const listPath = useRelativePath(name, { isAbsolutePath })
|
|
13
|
-
// To avoid watch being recalled
|
|
14
14
|
const listPathStr = listPath.join('$NEKOJOIN$')
|
|
15
|
-
const error = form.getError(listPath)
|
|
15
|
+
const [error, setError] = React.useState(form.getError(listPath))
|
|
16
16
|
|
|
17
17
|
// Counter to generate unique keys
|
|
18
18
|
const keyCounter = React.useRef(0)
|
|
19
19
|
// Map to track keys by value reference
|
|
20
20
|
const keysMap = React.useRef(new WeakMap())
|
|
21
21
|
|
|
22
|
+
// Register rules with the form
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
return form.registerRules(listPath, rules, validateTrigger)
|
|
25
|
+
}, [listPathStr, JSON.stringify(rules), validateTrigger])
|
|
26
|
+
|
|
27
|
+
// Listen for error changes
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
return form.registerErrorListener(listPath, (err) => setError(err))
|
|
30
|
+
}, [listPathStr])
|
|
31
|
+
|
|
22
32
|
const generateFields = (items) => {
|
|
23
33
|
if (!Array.isArray(items)) return []
|
|
24
34
|
return items.map((item, index) => {
|
|
@@ -47,14 +57,22 @@ export function FormList({ name, isAbsolutePath, children }) {
|
|
|
47
57
|
})
|
|
48
58
|
}, [listPathStr])
|
|
49
59
|
|
|
60
|
+
const validateOnChange = () => {
|
|
61
|
+
if (shouldValidateOn('onChange', rules, validateTrigger)) {
|
|
62
|
+
form.validateField(listPath, 'onChange')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
const add = (defaultValue = {}) => {
|
|
51
67
|
const current = form.getFieldValue(listPath) || []
|
|
52
68
|
form.setFieldValue(listPath, [...current, defaultValue])
|
|
69
|
+
validateOnChange()
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
const addOn = (index, defaultValue = {}) => {
|
|
56
73
|
const current = form.getFieldValue(listPath) || []
|
|
57
74
|
form.setFieldValue(listPath, [...current.slice(0, index), defaultValue, ...current.slice(index)])
|
|
75
|
+
validateOnChange()
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
const replace = (index, value) => {
|
|
@@ -91,6 +109,7 @@ export function FormList({ name, isAbsolutePath, children }) {
|
|
|
91
109
|
listPath,
|
|
92
110
|
current.filter((_, i) => i !== index)
|
|
93
111
|
)
|
|
112
|
+
validateOnChange()
|
|
94
113
|
}
|
|
95
114
|
|
|
96
115
|
const actions = React.useMemo(
|
|
@@ -102,7 +121,7 @@ export function FormList({ name, isAbsolutePath, children }) {
|
|
|
102
121
|
move,
|
|
103
122
|
duplicate,
|
|
104
123
|
}),
|
|
105
|
-
[listPathStr]
|
|
124
|
+
[listPathStr, rules, validateTrigger]
|
|
106
125
|
)
|
|
107
126
|
|
|
108
127
|
let content
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Button } from '../actions/Button'
|
|
2
2
|
import { useFormInstance, useFormState } from './Form'
|
|
3
3
|
|
|
4
|
-
export function SubmitButton({ form, disabled, ...props }) {
|
|
4
|
+
export function SubmitButton({ form, disabled, Wrapper, ...props }) {
|
|
5
5
|
const formState = useFormState()
|
|
6
6
|
const contextForm = useFormInstance()
|
|
7
7
|
form = form || contextForm
|
|
8
8
|
disabled = formState?.disabled || disabled
|
|
9
9
|
|
|
10
|
+
Wrapper = Wrapper || Button
|
|
11
|
+
|
|
10
12
|
const handleSubmit = () => {
|
|
11
13
|
if (!form) {
|
|
12
14
|
console.error('No form provided to useWatch. Pass it as params or wrap it inside a <Form> component.')
|
|
@@ -16,5 +18,5 @@ export function SubmitButton({ form, disabled, ...props }) {
|
|
|
16
18
|
form.handleSubmit()
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
return <
|
|
21
|
+
return <Wrapper {...props} disabled={disabled} onPress={handleSubmit} />
|
|
20
22
|
}
|
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
import { assocPath, path } from 'ramda'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { validateRules, validateAllFields, normalizeRules } from './validation'
|
|
3
4
|
|
|
4
5
|
export function useNewForm({ initialValues = {}, validate, onSubmit } = {}) {
|
|
5
6
|
const valuesRef = React.useRef({ ...initialValues })
|
|
6
|
-
const errorsRef = React.useRef({})
|
|
7
|
+
const errorsRef = React.useRef({}) // Flat structure: { 'users': 'error', 'users.0.name': 'error' }
|
|
7
8
|
const listenersRef = React.useRef({})
|
|
9
|
+
const errorListenersRef = React.useRef({})
|
|
10
|
+
const rulesRegistryRef = React.useRef(new Map())
|
|
8
11
|
|
|
9
12
|
const formApi = React.useMemo(() => {
|
|
13
|
+
const toKey = (name) => (Array.isArray(name) ? name.join('.') : name)
|
|
14
|
+
const toPath = (name) => (Array.isArray(name) ? name : [name])
|
|
15
|
+
|
|
10
16
|
const notify = (name) => {
|
|
11
|
-
const key =
|
|
17
|
+
const key = toKey(name)
|
|
12
18
|
if (listenersRef.current[key]) {
|
|
13
|
-
listenersRef.current[key].forEach((cb) => cb(path(name, valuesRef.current)))
|
|
19
|
+
listenersRef.current[key].forEach((cb) => cb(path(toPath(name), valuesRef.current)))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const notifyError = (name) => {
|
|
24
|
+
const key = toKey(name)
|
|
25
|
+
if (errorListenersRef.current[key]) {
|
|
26
|
+
errorListenersRef.current[key].forEach((cb) => cb(errorsRef.current[key]))
|
|
14
27
|
}
|
|
15
28
|
}
|
|
16
29
|
|
|
17
30
|
const setFieldValue = (name, value) => {
|
|
18
|
-
valuesRef.current = assocPath(name, value, valuesRef.current)
|
|
31
|
+
valuesRef.current = assocPath(toPath(name), value, valuesRef.current)
|
|
19
32
|
notify(name)
|
|
20
33
|
}
|
|
21
34
|
|
|
22
|
-
const getFieldValue = (name) => path(name, valuesRef.current)
|
|
35
|
+
const getFieldValue = (name) => path(toPath(name), valuesRef.current)
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
// Flat error lookup by key
|
|
38
|
+
const getError = (name) => {
|
|
39
|
+
const key = toKey(name)
|
|
40
|
+
return errorsRef.current[key]
|
|
41
|
+
}
|
|
25
42
|
|
|
26
43
|
const setError = (name, error) => {
|
|
27
|
-
|
|
44
|
+
const key = toKey(name)
|
|
45
|
+
if (error) {
|
|
46
|
+
errorsRef.current[key] = error
|
|
47
|
+
} else {
|
|
48
|
+
delete errorsRef.current[key]
|
|
49
|
+
}
|
|
50
|
+
notifyError(name)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const clearErrors = () => {
|
|
54
|
+
errorsRef.current = {}
|
|
28
55
|
}
|
|
29
56
|
|
|
30
57
|
const registerListener = (name, cb) => {
|
|
31
|
-
const key =
|
|
58
|
+
const key = toKey(name)
|
|
32
59
|
if (!listenersRef.current[key]) {
|
|
33
60
|
listenersRef.current[key] = []
|
|
34
61
|
}
|
|
@@ -38,15 +65,77 @@ export function useNewForm({ initialValues = {}, validate, onSubmit } = {}) {
|
|
|
38
65
|
}
|
|
39
66
|
}
|
|
40
67
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
const registerErrorListener = (name, cb) => {
|
|
69
|
+
const key = toKey(name)
|
|
70
|
+
if (!errorListenersRef.current[key]) {
|
|
71
|
+
errorListenersRef.current[key] = []
|
|
72
|
+
}
|
|
73
|
+
errorListenersRef.current[key].push(cb)
|
|
74
|
+
return () => {
|
|
75
|
+
errorListenersRef.current[key] = errorListenersRef.current[key].filter((fn) => fn !== cb)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const registerRules = (name, rules, defaultTrigger = 'onSubmit') => {
|
|
80
|
+
if (!rules) return
|
|
81
|
+
const key = toKey(name)
|
|
82
|
+
const rulesArray = normalizeRules(rules).map((rule) => ({
|
|
83
|
+
...rule,
|
|
84
|
+
trigger: rule.trigger || defaultTrigger,
|
|
85
|
+
}))
|
|
86
|
+
rulesRegistryRef.current.set(key, { path: name, rules: rulesArray })
|
|
87
|
+
return () => rulesRegistryRef.current.delete(key)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const validateField = async (name, trigger = 'onSubmit') => {
|
|
91
|
+
const key = toKey(name)
|
|
92
|
+
const entry = rulesRegistryRef.current.get(key)
|
|
93
|
+
if (!entry) return null
|
|
94
|
+
|
|
95
|
+
const value = path(name, valuesRef.current)
|
|
96
|
+
const error = await validateRules(value, entry.rules, trigger)
|
|
97
|
+
|
|
98
|
+
if (error) {
|
|
99
|
+
errorsRef.current[key] = error
|
|
100
|
+
} else {
|
|
101
|
+
delete errorsRef.current[key]
|
|
102
|
+
}
|
|
103
|
+
notifyError(name)
|
|
104
|
+
return error
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const validateForm = async () => {
|
|
108
|
+
// Clear previous errors
|
|
109
|
+
errorsRef.current = {}
|
|
110
|
+
|
|
111
|
+
// Run rules-based validation
|
|
112
|
+
const rulesErrors = await validateAllFields(valuesRef.current, rulesRegistryRef.current)
|
|
113
|
+
|
|
114
|
+
// Run legacy validate function if provided
|
|
115
|
+
const legacyErrors = validate ? validate(valuesRef.current) || {} : {}
|
|
116
|
+
|
|
117
|
+
// Store errors in flat structure
|
|
118
|
+
Object.entries(rulesErrors).forEach(([key, error]) => {
|
|
119
|
+
errorsRef.current[key] = error
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Legacy errors are already flat (or should be converted)
|
|
123
|
+
Object.entries(legacyErrors).forEach(([key, error]) => {
|
|
124
|
+
if (!errorsRef.current[key]) {
|
|
125
|
+
errorsRef.current[key] = error
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Notify all error listeners
|
|
130
|
+
rulesRegistryRef.current.forEach((_, key) => {
|
|
131
|
+
notifyError(key)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return Object.keys(errorsRef.current).length === 0
|
|
46
135
|
}
|
|
47
136
|
|
|
48
|
-
const handleSubmit = () => {
|
|
49
|
-
const isValid = validateForm()
|
|
137
|
+
const handleSubmit = async () => {
|
|
138
|
+
const isValid = await validateForm()
|
|
50
139
|
if (!isValid) return
|
|
51
140
|
console.log('SUBMIT')
|
|
52
141
|
onSubmit(valuesRef.current)
|
|
@@ -57,7 +146,11 @@ export function useNewForm({ initialValues = {}, validate, onSubmit } = {}) {
|
|
|
57
146
|
getFieldValue,
|
|
58
147
|
getError,
|
|
59
148
|
setError,
|
|
149
|
+
clearErrors,
|
|
60
150
|
registerListener,
|
|
151
|
+
registerErrorListener,
|
|
152
|
+
registerRules,
|
|
153
|
+
validateField,
|
|
61
154
|
handleSubmit,
|
|
62
155
|
valuesRef,
|
|
63
156
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const defaultMessages = {
|
|
2
|
+
required: 'This field is required',
|
|
3
|
+
type: {
|
|
4
|
+
email: 'Please enter a valid email address',
|
|
5
|
+
url: 'Please enter a valid URL',
|
|
6
|
+
number: 'Please enter a valid number',
|
|
7
|
+
integer: 'Please enter a valid integer',
|
|
8
|
+
},
|
|
9
|
+
min: {
|
|
10
|
+
string: (min) => `Must be at least ${min} characters`,
|
|
11
|
+
number: (min) => `Must be at least ${min}`,
|
|
12
|
+
array: (min) => `Must have at least ${min} items`,
|
|
13
|
+
},
|
|
14
|
+
max: {
|
|
15
|
+
string: (max) => `Must be at most ${max} characters`,
|
|
16
|
+
number: (max) => `Must be at most ${max}`,
|
|
17
|
+
array: (max) => `Must have at most ${max} items`,
|
|
18
|
+
},
|
|
19
|
+
pattern: 'Invalid format',
|
|
20
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { defaultMessages } from './defaultMessages'
|
|
2
|
+
export { validators } from './validators'
|
|
3
|
+
export { validateRules, validateAllFields } from './validateRules'
|
|
4
|
+
export { normalizeRules } from './normalizeRules'
|
|
5
|
+
export { shouldValidateOn } from './shouldValidateOn'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes rules to array format.
|
|
3
|
+
* Accepts either array format or object shorthand.
|
|
4
|
+
*
|
|
5
|
+
* Array format (full control):
|
|
6
|
+
* [{ required: true, message: 'Required' }, { min: 2 }]
|
|
7
|
+
*
|
|
8
|
+
* Object shorthand (simple cases):
|
|
9
|
+
* { required: true, min: 2, max: 7, type: 'email' }
|
|
10
|
+
* -> converts to: [{ required: true }, { min: 2 }, { max: 7 }, { type: 'email' }]
|
|
11
|
+
*
|
|
12
|
+
* @param {Array|Object} rules
|
|
13
|
+
* @returns {Array}
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeRules(rules) {
|
|
16
|
+
if (!rules) return []
|
|
17
|
+
if (Array.isArray(rules)) return rules
|
|
18
|
+
if (typeof rules === 'object') {
|
|
19
|
+
return Object.entries(rules).map(([key, value]) => ({ [key]: value }))
|
|
20
|
+
}
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeRules } from './normalizeRules'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if validation should run for a given trigger.
|
|
5
|
+
* @param {string} trigger - The trigger to check ('onChange', 'onBlur', 'onSubmit')
|
|
6
|
+
* @param {Array|Object} rules - The rules (array or object format)
|
|
7
|
+
* @param {string|string[]} validateTrigger - The default trigger(s) for the field
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function shouldValidateOn(trigger, rules, validateTrigger = 'onSubmit') {
|
|
11
|
+
if (!rules) return false
|
|
12
|
+
|
|
13
|
+
const triggers = Array.isArray(validateTrigger) ? validateTrigger : [validateTrigger]
|
|
14
|
+
if (triggers.includes(trigger)) return true
|
|
15
|
+
|
|
16
|
+
// Check per-rule triggers
|
|
17
|
+
const rulesArray = normalizeRules(rules)
|
|
18
|
+
return rulesArray.some(
|
|
19
|
+
(rule) => rule.trigger === trigger || (Array.isArray(rule.trigger) && rule.trigger.includes(trigger))
|
|
20
|
+
)
|
|
21
|
+
}
|