Skip to content

Commit

Permalink
Merge pull request #14 from davidnguyen179/refactor
Browse files Browse the repository at this point in the history
Refactor code & Add onLoad event
  • Loading branch information
davidnguyen11 authored Dec 25, 2019
2 parents a207524 + b39d7af commit aec6e30
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 41 deletions.
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -64,7 +65,12 @@ class App extends React.Component {
return (
<div>
<div style={{ marginTop: '50%' }}>
<Viewport type="fit" onEnter={this.onEnterRed} onLeave={this.onLeaveRed}>
<Viewport
type="fit"
onLoad={this.onLoadRed}
onEnter={this.onEnterRed}
onLeave={this.onLeaveRed}
>
<div style={{ height: 100, background: 'red' }}></div>
</Viewport>
</div>
Expand All @@ -78,6 +84,10 @@ class App extends React.Component {
);
}

onLoadRed = () => {
console.log('component RED loaded')
};

onEnterRed = (enterTimes) => {
console.log('enter red', enterTimes);
};
Expand Down Expand Up @@ -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}
Expand All @@ -133,6 +144,10 @@ class App extends React.Component {
);
}

onLoadRed = () => {
console.log('component RED loaded')
};

onEnterRed = (enterTimes) => {
console.log('enter red', enterTimes);
};
Expand Down
9 changes: 9 additions & 0 deletions src/__tests__/viewport.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('testing Viewport component', () => {
let clock;
let isFittedIn;
let isOverlapping;
let onLoad;
let onEnter;
let onLeave;
let onFocusOut;
Expand All @@ -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();
Expand All @@ -63,18 +65,25 @@ describe('testing Viewport component', () => {
it('should should increase enterCount to 2 && leaveCount 1 with "fit" mode - window', () => {
const wrapper = mount(
<Viewport
onLoad={onLoad}
onEnter={enterCount => onEnter(enterCount)}
onLeave={leaveCount => onLeave(leaveCount)}
>
hello world
</Viewport>
);
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
Expand Down
15 changes: 14 additions & 1 deletion src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class App extends React.Component<{}, State> {
<Viewport
autoTrack
type="fit"
onLoad={this.onLoadRed}
onEnter={this.onEnterRed}
onLeave={this.onLeaveRed}
onFocusOut={this.onFocusOut}
Expand All @@ -33,7 +34,12 @@ class App extends React.Component<{}, State> {
</div>

<div style={{ marginTop: '200%' }}>
<Viewport type="overlap" onEnter={this.onEnterBlue} onLeave={this.onLeaveBlue}>
<Viewport
type="overlap"
onLoad={this.onLoadBlue}
onEnter={this.onEnterBlue}
onLeave={this.onLeaveBlue}
>
<div style={{ height: 100, background: 'blue' }}></div>
<div>Enter component: {this.state.enterBlue}</div>
<div>Leave component: {this.state.leaveBlue}</div>
Expand All @@ -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 });
Expand All @@ -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 });
Expand Down
136 changes: 100 additions & 36 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,50 @@ export class Viewport extends React.Component<Props> {

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];
}

public componentDidMount() {
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 {
Expand All @@ -66,23 +75,50 @@ export class Viewport extends React.Component<Props> {
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();
}
};

Expand All @@ -92,32 +128,46 @@ export class Viewport extends React.Component<Props> {
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;
Expand All @@ -140,19 +190,33 @@ 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 {
width: number;
height: number;
}

interface Counter {
enter?: number;
focus?: number;
leave?: number;
}

interface Flag {
isEntered?: boolean;
isLeft?: boolean;
isVisited?: boolean;
}

type Type = 'fit' | 'overlap';

type Func = {
Expand Down

0 comments on commit aec6e30

Please sign in to comment.