From b39d7af4723dc6cb9792ca8600efa8a98e1064ed Mon Sep 17 00:00:00 2001 From: davidnguyen179 Date: Wed, 25 Dec 2019 17:07:40 +0900 Subject: [PATCH] Refactor code & Add onLoad event --- README.md | 23 +++++- src/__tests__/viewport.test.tsx | 9 +++ src/app/index.tsx | 15 +++- src/index.tsx | 136 +++++++++++++++++++++++--------- 4 files changed, 142 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index bd93ee5..5c2f5ee 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,10 @@ Here is how `type = 'fit'` and `type = 'overlap'` look like: | Name| Type | Parameter | Description |--|--|--|--| -| onEnter | void | enterCount | When component is in the Viewport, the `enterCount` increase 1 | -| onLeave | void | leaveCount | When component is not in the Viewport, the `leaveCount` increase 1 | -| onFocusOut | void | focusCount | When component is not in the Viewport, then `onFocusOut` called with seconds user spent on the component | +| onLoad | void | | When component first appears in the viewport | +| onEnter | void | enterCount | When scrolling to a component, the `enterCount` increase 1 | +| onLeave | void | leaveCount | When scrolling away from a component, the `leaveCount` increase 1 | +| onFocusOut | void | focusCount | When component is not in the viewport, then `onFocusOut` called with seconds user spent on the component | ## Example @@ -64,7 +65,12 @@ class App extends React.Component { return (
- +
@@ -78,6 +84,10 @@ class App extends React.Component { ); } + onLoadRed = () => { + console.log('component RED loaded') + }; + onEnterRed = (enterTimes) => { console.log('enter red', enterTimes); }; @@ -116,6 +126,7 @@ class App extends React.Component { autoTrack waitToStartAutoTrack={2} type="fit" + onLoad={this.onLoadRed} onEnter={this.onEnterRed} onLeave={this.onLeaveRed} onFocusOut={this.onFocusOut} @@ -133,6 +144,10 @@ class App extends React.Component { ); } + onLoadRed = () => { + console.log('component RED loaded') + }; + onEnterRed = (enterTimes) => { console.log('enter red', enterTimes); }; diff --git a/src/__tests__/viewport.test.tsx b/src/__tests__/viewport.test.tsx index 74d0fb4..2fb008c 100644 --- a/src/__tests__/viewport.test.tsx +++ b/src/__tests__/viewport.test.tsx @@ -28,6 +28,7 @@ describe('testing Viewport component', () => { let clock; let isFittedIn; let isOverlapping; + let onLoad; let onEnter; let onLeave; let onFocusOut; @@ -41,6 +42,7 @@ describe('testing Viewport component', () => { clock = sandboxes.useFakeTimers(); isFittedIn = sandboxes.stub(Utils, 'isFittedIn'); isOverlapping = sandboxes.stub(Utils, 'isOverlapping'); + onLoad = sandboxes.spy(); onEnter = sandboxes.spy(); onLeave = sandboxes.spy(); onFocusOut = sandboxes.spy(); @@ -63,18 +65,25 @@ describe('testing Viewport component', () => { it('should should increase enterCount to 2 && leaveCount 1 with "fit" mode - window', () => { const wrapper = mount( onEnter(enterCount)} onLeave={leaveCount => onLeave(leaveCount)} > hello world ); + const instance = wrapper.instance(); + const { delay, type } = wrapper.props(); expect(delay).toBe(100); expect(type).toBe('fit'); // Enter component first time isFittedIn.returns(true); + + instance.componentDidMount(); + expect(onLoad.called).toBeTruthy(); + eventMap.scroll(); // Wait for throttling delay diff --git a/src/app/index.tsx b/src/app/index.tsx index ffa4829..5feebb9 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -21,6 +21,7 @@ class App extends React.Component<{}, State> { {
- +
Enter component: {this.state.enterBlue}
Leave component: {this.state.leaveBlue}
@@ -43,6 +49,10 @@ class App extends React.Component<{}, State> { ); } + private onLoadRed = () => { + console.log('component red loaded'); + }; + private onEnterRed = (enterRed: number) => { console.log('enter red', enterRed); this.setState({ enterRed }); @@ -58,6 +68,9 @@ class App extends React.Component<{}, State> { this.setState({ focusRed }); }; + private onLoadBlue = () => { + console.log('component blue loaded'); + }; private onEnterBlue = (enterBlue: number) => { console.log('enter blue', enterBlue); this.setState({ enterBlue }); diff --git a/src/index.tsx b/src/index.tsx index 6e04da2..9acd793 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -16,29 +16,32 @@ export class Viewport extends React.Component { private isInScreen: (size: Size, rect: DOMRect) => boolean; - private enterCount: number; + private timer: number; - private focusCount: number; + private counter: Counter; - private counter: number; + private flag: Flag; - private isEntered: boolean; + constructor(props: Props) { + super(props); - private leaveCount: number; + this.counter = { + enter: 0, + focus: 0, + leave: 0 + }; - private isLeft: boolean; + this.flag = { + isEntered: false, + isLeft: false, + isVisited: false + }; - constructor(props: Props) { - super(props); - this.enterCount = 0; - this.focusCount = 0; - this.isEntered = false; - this.leaveCount = 0; - this.isLeft = false; const func: Func = { fit: isFittedIn, overlap: isOverlapping }; + this.isInScreen = func[this.props.type]; } @@ -46,11 +49,17 @@ export class Viewport extends React.Component { window.addEventListener('scroll', throttle(this.props.delay, this.handleScroll), { passive: true }); + /* + * Check component is in viewport + * Start increase enter times + * Start the counter if `autoTrack` turns on + */ + this.handleLoad(); } public componentWillUnmount() { window.removeEventListener('scroll', this.handleScroll); - this.resetCounter(); + this.resetTimer(); } public render(): JSX.Element { @@ -66,23 +75,50 @@ export class Viewport extends React.Component { const rect = this.screenRef.current.getBoundingClientRect(); if (this.isInScreen(size, rect)) { - if (!this.isEntered && this.props.onEnter) { - this.enterCount++; - this.props.onEnter(this.enterCount); - this.isEntered = true; - this.isLeft = false; + this.handleEnter(); + } else { + this.handleLeave(); + } + }; + + private handleLoad = () => { + this.handleEnter(() => { + this.setInternalFlag({ isVisited: true }); + if (this.props.onLoad) { + this.props.onLoad(); + } + }); + }; + + private handleEnter = (callback?: () => void) => { + const size = this.getWidthHeight(); + const rect = this.screenRef.current.getBoundingClientRect(); + + if (this.isInScreen(size, rect)) { + if (!this.flag.isEntered && !this.flag.isVisited && this.props.onEnter) { + this.setInternalCounter({ enter: this.counter.enter + 1 }); + this.setInternalFlag({ isLeft: false, isEntered: true }); + this.props.onEnter(this.counter.enter); + callback && callback(); /* * Only start the auto track when it is in the `fit` mode */ - this.props.type === 'fit' && this.startCounter(); + this.props.type === 'fit' && this.startTimer(); } - } else if (this.isEntered && !this.isLeft && this.props.onLeave) { - this.leaveCount++; - this.props.onLeave(this.leaveCount); - this.isLeft = true; - this.isEntered = false; - this.resetCounter(); + } + }; + + private handleLeave = () => { + if (this.flag.isEntered && !this.flag.isLeft && this.props.onLeave) { + this.setInternalCounter({ leave: this.counter.leave + 1 }); + this.setInternalFlag({ + isLeft: true, + isEntered: false, + isVisited: false + }); + this.props.onLeave(this.counter.leave); + this.resetTimer(); } }; @@ -92,32 +128,46 @@ export class Viewport extends React.Component { return { width, height }; } - private async startCounter() { + private async startTimer() { if (this.props.autoTrack === false) return; if (this.props.waitToStartAutoTrack > 0) { await this.sleep(); } - this.counter = window.setInterval(() => { - this.focusCount++; + this.timer = window.setInterval(() => { + this.setInternalCounter({ focus: this.counter.focus + 1 }); }, 1000); } - private resetCounter() { + private resetTimer() { if (!this.props.autoTrack) return; if (this.props.onFocusOut) { - this.props.onFocusOut(this.focusCount); + this.props.onFocusOut(this.counter.focus); } - clearInterval(this.counter); - this.focusCount = 0; + clearInterval(this.timer); + this.setInternalCounter({ focus: 0 }); } private sleep() { return new Promise(resolve => setTimeout(resolve, this.props.waitToStartAutoTrack * 1000)); } + + private setInternalFlag = (flag: Flag) => { + this.flag = { + ...this.flag, + ...flag + }; + }; + + private setInternalCounter = (counter: Counter) => { + this.counter = { + ...this.counter, + ...counter + }; + }; } type Props = DataProps & EventProps; @@ -140,12 +190,14 @@ interface DataProps { } interface EventProps { + /** When component is mounted */ + onLoad?: () => void; /** When component is in the viewport, event will be executed */ - onEnter: (enterCount?: number) => void; + onEnter?: (enterTimes?: number) => void; /** When component is not in the viewport, event will be executed */ - onLeave?: (leaveCount?: number) => void; + onLeave?: (leaveTimes?: number) => void; /** When component is not focused the viewport, event will be executed */ - onFocusOut?: (focusCount?: number) => void; + onFocusOut?: (focusTimes?: number) => void; } export interface Size { @@ -153,6 +205,18 @@ export interface Size { height: number; } +interface Counter { + enter?: number; + focus?: number; + leave?: number; +} + +interface Flag { + isEntered?: boolean; + isLeft?: boolean; + isVisited?: boolean; +} + type Type = 'fit' | 'overlap'; type Func = {