diff --git a/README.md b/README.md index 53b95c6..ee4dab1 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,57 @@ If you find any problem, you can open an issue, I will resolve it as soon as pos If you have any suggestion, please also open an issue, it will be greatly appreciated! +## Subscribable Data +Issues arise when trying to use dynamic *maxVisibleSlides* property with dynamics items +(see details)[https://github.com/BotDanny/react-stacked-center-carousel/issues/22] +To work around this use subscribable data object +``` +import React, { useEffect } from "react"; +import { PromisedData, ResponsiveContainer, StackedCarousel } from "react-stacked-center-carousel/src"; + +// component for individual slides +const CarouselItem = React.memo(function (props: any) { + const item = props.data[props.dataIndex]; + return ( + {item} + ); +}); + +// new data from Promise, RxJs Observable, fetch api, w/e +const fetchedItems = new Promise((resolve, reject) => { + setTimeout(() => resolve(['one', 'two', 'three']), 1000); +}); + +// PromisedData object with initial items; RxJs BehaviorSubject can also be used +const promisedItems = new PromisedData(['one', 'two']); + +export default function CarouselExample () { + const ref = React.useRef(); + + useEffect(() => { + fetchedItems.then(newItems => promisedItems.next(newItems)) + }, []); + + return ( + { + return ( + = 3 ? 3 : 1} + useGrabCursor + /> + ); + }} + /> + ); +} +``` ## License diff --git a/src/carousel.tsx b/src/carousel.tsx index 9d3da8a..a5b4f91 100644 --- a/src/carousel.tsx +++ b/src/carousel.tsx @@ -6,6 +6,7 @@ import { staticSlideInfo, renderedSlide } from './interfaces'; +import { IPromisedSub } from './promiseddata'; export default class StackedCarousel extends React.PureComponent { static defaultScaleFactor: number = 0.85; @@ -21,16 +22,19 @@ export default class StackedCarousel extends React.PureComponent { private centerPosition: number; private maxZIndex: number; private renderedSlidePerSide: number; - + private subscription?: IPromisedSub; + private validateProps = () => { const { currentVisibleSlide, maxVisibleSlide, fadeDistance, customScales, - data + data, + promisedData } = this.props; - if (data.length < (maxVisibleSlide + 1) / 2) { + + if ((promisedData ? promisedData.value : data).length < (maxVisibleSlide + 1) / 2) { throw Error( 'you must have more than (maxVisibleSlide + 1) / 2 data item' ); @@ -57,6 +61,10 @@ export default class StackedCarousel extends React.PureComponent { }; private initializeProperties = (constructor: boolean = false) => { + // recalculate everything as if newly constructed + if (this.state?.dataUpdated) + constructor = true; + this.validateProps(); const { carouselWidth, @@ -78,7 +86,7 @@ export default class StackedCarousel extends React.PureComponent { : this.state.renderedSlides.filter(({ slideIndex, dataIndex }) => { return dataIndex === -1 || Math.abs(slideIndex) <= this.slidePerSide; }); - const slideInfoMap = {}; + const slideInfoMap: any = {}; const newCenterSlideRelativeIndex = constructor ? (totalRenderCount - 1) / 2 @@ -241,6 +249,11 @@ export default class StackedCarousel extends React.PureComponent { constructor(props: props) { super(props); + // mutually exclusive objects + if (props.promisedData && props.data) { + console.warn('`dataPromise` defined. `data` property will be ignored.'); + } + const { renderedSlides, slideInfoMap, @@ -257,7 +270,7 @@ export default class StackedCarousel extends React.PureComponent { this.height = this.props.height || 0; this.listRef = React.createRef(); this.clearSlideTimeout = null; - this.keyCount = props.data.length; + this.keyCount = (props.promisedData ? props.promisedData.value : props.data).length; this.addedSlide = 0; this.centerPosition = centerPosition; this.maxZIndex = 100; @@ -279,11 +292,15 @@ export default class StackedCarousel extends React.PureComponent { } componentDidMount() { + this.subscription = this.props.promisedData?.subscribe(_ => { + this.setState({...this.state, dataUpdated: true}); + }); this.updateHeight(); } componentWillUnmount() { clearTimeout(this.clearSlideTimeout); + this.subscription?.unsubscribe(); } componentDidUpdate(prevProps: props) { @@ -326,8 +343,8 @@ export default class StackedCarousel extends React.PureComponent { }; private modDataRange = (n: number) => { - const { data } = this.props; - const m = data.length; + const { data, promisedData } = this.props; + const m = (promisedData ? promisedData.value : data).length; return ((n % m) + m) % m; }; @@ -697,6 +714,7 @@ export default class StackedCarousel extends React.PureComponent { transitionTime = StackedCarousel.defaultTransitionTime, className, data, + promisedData, slideWidth, customTransition, carouselWidth, @@ -706,6 +724,8 @@ export default class StackedCarousel extends React.PureComponent { const cursor = (useGrabCursor && (swipeStarted ? 'grabbing' : 'grab')) || 'default'; + + let data2Use = promisedData ? promisedData.value : data; return (
{ {dataIndex !== -1 && ( void), error?: (error: any) => void, complete?: () => void): IPromisedSub; +} + +/** + * interface compatible with RXJS Subscription + */ +export interface IPromisedSub { + unsubscribe(): void; +} + +/** + * Fake listenable object compatible with RXJS BehaviorSubject + */ +export class PromisedData implements IPromisedData { + private _data: any = []; + private _handlers: ((data: any[]) => void)[] = []; + + get value() { return this._data; } + + constructor(initialData: any[]) { + this._data = initialData; + } + + next(data: any[]) { + this._data = data; + this._handlers.forEach(h => h?.call(this, data)); + } + + subscribe (completed: (data: any[]) => void): IPromisedSub { + this._handlers.push(completed); + + // immediately send last value + setTimeout(() => completed?.call(this, this._data), 1); + + return new PromisedSubscription(this, completed); + } + + removeHadler (handler: any) { + this._handlers = this._handlers.filter(h => h !== handler); + } +} + +class PromisedSubscription implements IPromisedSub { + private _parent: PromisedData; + private _handler: any; + + constructor(parent: PromisedData, handler: any) { + this._handler = handler; + this._parent = parent; + } + + unsubscribe() { + this._parent.removeHadler(this._handler); + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1ff2baf..9df7a32 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "noImplicitThis": true, "noImplicitAny": true, "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, + // "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": false, "noUnusedParameters": true, "allowSyntheticDefaultImports": true