Skip to content

Commit

Permalink
feat: use context to provide more convinient way to interact with car…
Browse files Browse the repository at this point in the history
…ousel
  • Loading branch information
r0b0t3d committed Jun 12, 2021
1 parent 0378473 commit 0479d4d
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 64 deletions.
100 changes: 61 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,55 +64,66 @@ Note: Currently, I am using `react-native-reanimated` for animation. So you shou
```
## Usage
```javascript
import Carousel from '@r0b0t3d/react-native-carousel';
import Carousel, {
withCarouselContext,
useCarouselContext,
} from '@r0b0t3d/react-native-carousel';

function MyCarousel() {
const currentPage = useSharedValue(0);
const {
goNext,
goPrev,
snapToItem
} = useCarouselContext(); // <- use this instead of passing ref to Carousel

return (
<View>
<Carousel
style={{ height: 200 }}
data={data}
loop={false}
autoPlay={true}
duration={3000}
itemWidth={width - 100}
inactiveOpacity={0.5}
inactiveScale={0.9}
firstItemAlignment="start"
spaceBetween={20}
animatedPage={currentPage}
renderItem={({item}) => {
return (
<Image
style={{
flex: 1,
backgroundColor: 'red',
}}
source={{ uri: item.url }}
/>
);
}}
/>
<View>
<PaginationIndicator
totalPage={data.length}
currentPage={currentPage}
containerStyle={{ marginTop: 20 }}
activeIndicatorStyle={{
width: 20,
height: 10,
borderRadius: 5,
}}
indicatorConfigs={{
spaceBetween: 10
<Carousel
style={{ height: 200 }}
data={data}
loop={false}
autoPlay={true}
duration={3000}
itemWidth={width - 100}
inactiveOpacity={0.5}
inactiveScale={0.9}
firstItemAlignment="start"
spaceBetween={20}
animatedPage={currentPage}
renderItem={({item}) => {
return (
<Image
style={{
flex: 1,
backgroundColor: 'red',
}}
source={{ uri: item.url }}
/>
);
}}
/>
<View>
<PaginationIndicator
containerStyle={{ marginTop: 20 }}
activeIndicatorStyle={{
height: 10,
borderRadius: 5,
}}
indicatorConfigs={{
spaceBetween: 10,
indicatorWidth: 10,
indicatorSelectedWidth: 20,
}}
/>
</View>
</View>
</View>
);
}

export default withCarouselContext(MyCarousel) // <-- To use carousel context, you need wrap your component with withCarouselContext
```

# Carousel
## Properties

| Props | Description | Default |
Expand Down Expand Up @@ -142,6 +153,17 @@ function MyCarousel() {
| goNext | Go to next index |
| goPrev | Go to previous index |
| snapToItem | `(index: number, animated?: boolean) => void`<br>Snap to specific index <br>- `index`: destination index<br>- `animated`: should animate or not, default is `true` |
### withCarouselContext
This HOC provides easy way to wrap your component with `CarouselContext.Provider`.
So if you'd like to use `useCarouselContext`, you need to wrap your component with this.

# PaginationIndicator
Easy way to define the indicator for your carousel.

Please note that, this component only works with `withCarouselContext`.
So please make sure that it is rendered under the component that you wrap with `withCarouselContext`

Check example above for more info
## Contributing

See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
Expand Down
18 changes: 9 additions & 9 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
import Carousel, {
CarouselHandles,
PaginationIndicator,
withCarouselContext,
useCarouselContext,
} from '@r0b0t3d/react-native-carousel';
import { useSharedValue } from 'react-native-reanimated';

Expand Down Expand Up @@ -78,27 +80,25 @@ const data: CarouselData[] = [

const { width } = Dimensions.get('window');

export default function App() {
const currentPage = useSharedValue(0);
const carousel = useRef<CarouselHandles>(null);
function App() {
const { goNext, goPrev, snapToItem } = useCarouselContext();

const handleRandom = useCallback(() => {
const randomIdx = Math.floor(Math.random() * data.length);
carousel.current?.snapToItem(randomIdx, true);
snapToItem(randomIdx, true);
}, []);

const handleNext = useCallback(() => {
carousel.current?.goNext();
goNext();
}, []);

const handlePrev = useCallback(() => {
carousel.current?.goPrev();
goPrev();
}, []);

return (
<View style={styles.container}>
<Carousel
ref={carousel}
style={{ height: 200 }}
initialPage={2}
data={data}
Expand All @@ -111,7 +111,6 @@ export default function App() {
firstItemAlignment="center"
spaceBetween={10}
spaceHeadTail={20}
animatedPage={currentPage}
additionalPagesPerSide={3}
scrollViewProps={{
scrollEnabled: true,
Expand All @@ -131,7 +130,6 @@ export default function App() {
<View>
<PaginationIndicator
totalPage={data.length}
currentPage={currentPage}
containerStyle={{ marginTop: 20 }}
activeIndicatorStyle={{
height: 20,
Expand Down Expand Up @@ -160,6 +158,8 @@ export default function App() {
);
}

export default withCarouselContext(App);

const styles = StyleSheet.create({
container: {
flex: 1,
Expand Down
27 changes: 23 additions & 4 deletions src/components/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, {
useState,
useRef,
Expand All @@ -18,6 +19,8 @@ import Animated, {
import type { CarouselProps, CarouselHandles } from '../types';
import PageItem from './PageItem';
import { findNearestPage, generateOffsets } from '../utils';
import { useCarouselContext } from './useCarouselContext';
import { useInternalCarouselContext } from './useInternalCarouselContext';

const { width: wWidth } = Dimensions.get('screen');

Expand All @@ -40,7 +43,6 @@ function Carousel<TData>(
spaceHeadTail = 0,
renderItem,
onPageChange,
animatedPage = useSharedValue(0),
scrollViewProps = {},
keyExtractor,
}: CarouselProps<TData>,
Expand All @@ -52,7 +54,8 @@ function Carousel<TData>(
const [isDragging, setDragging] = useState(false);
const expectedPosition = useRef(-1);
const pageMapper = useRef<Record<number, number>>({});

const { currentPage: animatedPage, totalPage } = useCarouselContext();

const horizontalPadding = useMemo(() => {
const padding = (sliderWidth - itemWidth) / 2;
return firstItemAlignment === 'center' || loop ? padding : spaceHeadTail;
Expand All @@ -76,6 +79,10 @@ function Carousel<TData>(
}, [sliderWidth, itemWidth, data, horizontalPadding]);

const pageItems = useMemo(() => {
if (!data) {
return [];
}
totalPage.value = data.length;
if (loop) {
const headItems = data.slice(
data.length - additionalPagesPerSide,
Expand Down Expand Up @@ -131,7 +138,7 @@ function Carousel<TData>(
[handleScrollTo, animatedScroll, freeze]
);

const goNext = useCallback(() => {
const goNext = useCallback(() => {
const next = currentPage.value + 1;
handleScrollTo(next);
}, [handleScrollTo]);
Expand Down Expand Up @@ -160,13 +167,25 @@ function Carousel<TData>(
[handleScrollTo]
);

const { setCarouselHandlers } = useInternalCarouselContext();

useEffect(() => {
if (setCarouselHandlers) {
setCarouselHandlers({
goNext,
goPrev,
snapToItem,
});
}
}, [goNext, goPrev, snapToItem, setCarouselHandlers]);

const handlePageChange = useCallback(
(page: number) => {
const actualPage = getActualPage(page);
animatedPage.value = actualPage;
if (onPageChange) {
onPageChange(actualPage);
}
}
if (!loop) return;
if (page === pageItems.length - 1) {
jumpTo(additionalPagesPerSide * 2 - 1);
Expand Down
62 changes: 62 additions & 0 deletions src/components/CarouselContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, {
FC,
ReactNode,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import Animated, { useSharedValue } from 'react-native-reanimated';
import type { CarouselHandles } from 'src/types';
import { CarouselContext } from './useCarouselContext';
import { InternalCarouselContext } from './useInternalCarouselContext';

type Props = {
children: ReactNode;
};

function CarouselContainer({ children }: Props) {
const carouselHandlers = useRef<CarouselHandles>();
const currentPage = useSharedValue(0);
const totalPage = useSharedValue(0);

const setCarouselHandlers = useCallback((handlers) => {
carouselHandlers.current = handlers;
}, []);

const context = useMemo(() => {
return {
goNext: () => carouselHandlers.current?.goNext(),
goPrev: () => carouselHandlers.current?.goPrev(),
snapToItem: (index: number, animated: boolean) =>
carouselHandlers.current?.snapToItem(index, animated),
currentPage,
totalPage,
};
}, []);

const internalContext = useMemo(
() => ({
setCarouselHandlers,
}),
[]
);

return (
<CarouselContext.Provider value={context}>
<InternalCarouselContext.Provider value={internalContext}>
{children}
</InternalCarouselContext.Provider>
</CarouselContext.Provider>
);
}

export default function withCarouselContext<T>(Component: FC<T>) {
return (props: T) => {
return (
<CarouselContainer>
<Component {...props} />
</CarouselContainer>
);
};
}

0 comments on commit 0479d4d

Please sign in to comment.