react-fathom 0.1.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/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/cjs/index.cjs +410 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/next/index.cjs +910 -0
- package/dist/cjs/next/index.cjs.map +1 -0
- package/dist/es/index.js +381 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/next/index.js +885 -0
- package/dist/es/next/index.js.map +1 -0
- package/dist/react-fathom.js +413 -0
- package/dist/react-fathom.js.map +1 -0
- package/dist/react-fathom.min.js +3 -0
- package/dist/react-fathom.min.js.map +1 -0
- package/package.json +127 -0
- package/src/FathomContext.tsx +5 -0
- package/src/FathomProvider.test.tsx +532 -0
- package/src/FathomProvider.tsx +122 -0
- package/src/components/TrackClick.test.tsx +191 -0
- package/src/components/TrackClick.tsx +62 -0
- package/src/components/TrackPageview.test.tsx +111 -0
- package/src/components/TrackPageview.tsx +36 -0
- package/src/components/TrackVisible.test.tsx +311 -0
- package/src/components/TrackVisible.tsx +105 -0
- package/src/components/index.ts +3 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useFathom.test.tsx +51 -0
- package/src/hooks/useFathom.ts +11 -0
- package/src/hooks/useTrackOnClick.test.tsx +197 -0
- package/src/hooks/useTrackOnClick.ts +65 -0
- package/src/hooks/useTrackOnMount.test.tsx +79 -0
- package/src/hooks/useTrackOnMount.ts +24 -0
- package/src/hooks/useTrackOnVisible.test.tsx +313 -0
- package/src/hooks/useTrackOnVisible.ts +99 -0
- package/src/index.ts +4 -0
- package/src/next/NextFathomProvider.test.tsx +131 -0
- package/src/next/NextFathomProvider.tsx +62 -0
- package/src/next/NextFathomProviderApp.test.tsx +308 -0
- package/src/next/NextFathomProviderApp.tsx +106 -0
- package/src/next/NextFathomProviderPages.test.tsx +330 -0
- package/src/next/NextFathomProviderPages.tsx +112 -0
- package/src/next/compositions/withAppRouter.test.tsx +113 -0
- package/src/next/compositions/withAppRouter.tsx +48 -0
- package/src/next/compositions/withPagesRouter.test.tsx +113 -0
- package/src/next/compositions/withPagesRouter.tsx +44 -0
- package/src/next/index.ts +7 -0
- package/src/next/types.ts +19 -0
- package/src/types.ts +37 -0
- package/types/FathomContext.d.ts +3 -0
- package/types/FathomContext.d.ts.map +1 -0
- package/types/FathomProvider.d.ts +5 -0
- package/types/FathomProvider.d.ts.map +1 -0
- package/types/components/TrackClick.d.ts +39 -0
- package/types/components/TrackClick.d.ts.map +1 -0
- package/types/components/TrackPageview.d.ts +21 -0
- package/types/components/TrackPageview.d.ts.map +1 -0
- package/types/components/TrackVisible.d.ts +39 -0
- package/types/components/TrackVisible.d.ts.map +1 -0
- package/types/components/index.d.ts +4 -0
- package/types/components/index.d.ts.map +1 -0
- package/types/hooks/index.d.ts +5 -0
- package/types/hooks/index.d.ts.map +1 -0
- package/types/hooks/useFathom.d.ts +6 -0
- package/types/hooks/useFathom.d.ts.map +1 -0
- package/types/hooks/useTrackOnClick.d.ts +39 -0
- package/types/hooks/useTrackOnClick.d.ts.map +1 -0
- package/types/hooks/useTrackOnMount.d.ts +14 -0
- package/types/hooks/useTrackOnMount.d.ts.map +1 -0
- package/types/hooks/useTrackOnVisible.d.ts +43 -0
- package/types/hooks/useTrackOnVisible.d.ts.map +1 -0
- package/types/index.d.ts +5 -0
- package/types/index.d.ts.map +1 -0
- package/types/next/AppRouterProvider.d.ts +7 -0
- package/types/next/AppRouterProvider.d.ts.map +1 -0
- package/types/next/NextFathomProvider.d.ts +34 -0
- package/types/next/NextFathomProvider.d.ts.map +1 -0
- package/types/next/NextFathomProviderApp.d.ts +6 -0
- package/types/next/NextFathomProviderApp.d.ts.map +1 -0
- package/types/next/NextFathomProviderPages.d.ts +6 -0
- package/types/next/NextFathomProviderPages.d.ts.map +1 -0
- package/types/next/PagesRouterProvider.d.ts +7 -0
- package/types/next/PagesRouterProvider.d.ts.map +1 -0
- package/types/next/compositions/withAppRouter.d.ts +29 -0
- package/types/next/compositions/withAppRouter.d.ts.map +1 -0
- package/types/next/compositions/withPagesRouter.d.ts +25 -0
- package/types/next/compositions/withPagesRouter.d.ts.map +1 -0
- package/types/next/index.d.ts +6 -0
- package/types/next/index.d.ts.map +1 -0
- package/types/next/types.d.ts +16 -0
- package/types/next/types.d.ts.map +1 -0
- package/types/test-setup.d.ts +2 -0
- package/types/test-setup.d.ts.map +1 -0
- package/types/types.d.ts +34 -0
- package/types/types.d.ts.map +1 -0
- package/types/useFathom.d.ts +7 -0
- package/types/useFathom.d.ts.map +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useEffect, useRef, forwardRef, useCallback } from 'react'
|
|
2
|
+
import type { ElementType, ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import type { EventOptions } from 'fathom-client'
|
|
5
|
+
|
|
6
|
+
import { useFathom } from '../hooks/useFathom'
|
|
7
|
+
|
|
8
|
+
export interface TrackVisibleProps extends EventOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Event name to track
|
|
11
|
+
*/
|
|
12
|
+
eventName: string
|
|
13
|
+
/**
|
|
14
|
+
* Child element(s) to render
|
|
15
|
+
*/
|
|
16
|
+
children?: ReactNode
|
|
17
|
+
/**
|
|
18
|
+
* Intersection observer options
|
|
19
|
+
*/
|
|
20
|
+
observerOptions?: IntersectionObserverInit
|
|
21
|
+
/**
|
|
22
|
+
* Whether to track only once or every time it becomes visible
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
trackOnce?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* HTML element to render as wrapper
|
|
28
|
+
* @default 'div'
|
|
29
|
+
*/
|
|
30
|
+
as?: ElementType
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Component that tracks an event when it becomes visible in the viewport
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <TrackVisible eventName="section-viewed" section="hero">
|
|
39
|
+
* <HeroSection />
|
|
40
|
+
* </TrackVisible>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const TrackVisible = forwardRef<HTMLDivElement, TrackVisibleProps>(
|
|
44
|
+
function TrackVisible(
|
|
45
|
+
{
|
|
46
|
+
eventName,
|
|
47
|
+
children,
|
|
48
|
+
observerOptions,
|
|
49
|
+
trackOnce = true,
|
|
50
|
+
as: Component = 'div',
|
|
51
|
+
...eventOptions
|
|
52
|
+
},
|
|
53
|
+
forwardedRef,
|
|
54
|
+
) {
|
|
55
|
+
const { trackEvent } = useFathom()
|
|
56
|
+
const internalRef = useRef<HTMLDivElement | null>(null)
|
|
57
|
+
const hasTracked = useRef(false)
|
|
58
|
+
|
|
59
|
+
// Callback ref that handles both forwarded and internal refs
|
|
60
|
+
const setRef = useCallback(
|
|
61
|
+
(node: HTMLDivElement | null) => {
|
|
62
|
+
internalRef.current = node
|
|
63
|
+
|
|
64
|
+
if (typeof forwardedRef === 'function') {
|
|
65
|
+
forwardedRef(node)
|
|
66
|
+
} else if (forwardedRef !== null && forwardedRef !== undefined) {
|
|
67
|
+
forwardedRef.current = node
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[forwardedRef],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const element = internalRef.current
|
|
75
|
+
if (element === null) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const observer = new IntersectionObserver(
|
|
80
|
+
(entries) => {
|
|
81
|
+
entries.forEach((entry) => {
|
|
82
|
+
if (entry.isIntersecting) {
|
|
83
|
+
if (!trackOnce || !hasTracked.current) {
|
|
84
|
+
trackEvent?.(eventName, eventOptions)
|
|
85
|
+
hasTracked.current = true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
threshold: 0.1,
|
|
92
|
+
...observerOptions,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
observer.observe(element)
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
observer.disconnect()
|
|
100
|
+
}
|
|
101
|
+
}, [eventName, eventOptions, observerOptions, trackOnce, trackEvent])
|
|
102
|
+
|
|
103
|
+
return <Component ref={setRef}>{children}</Component>
|
|
104
|
+
},
|
|
105
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { renderHook } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { FathomProvider } from '../FathomProvider'
|
|
8
|
+
import { useFathom } from './useFathom'
|
|
9
|
+
|
|
10
|
+
describe('useFathom', () => {
|
|
11
|
+
it('should return context values when used within FathomProvider', () => {
|
|
12
|
+
const mockClient = {
|
|
13
|
+
trackEvent: vi.fn(),
|
|
14
|
+
trackPageview: vi.fn(),
|
|
15
|
+
trackGoal: vi.fn(),
|
|
16
|
+
load: vi.fn(),
|
|
17
|
+
setSite: vi.fn(),
|
|
18
|
+
blockTrackingForMe: vi.fn(),
|
|
19
|
+
enableTrackingForMe: vi.fn(),
|
|
20
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
24
|
+
<FathomProvider client={mockClient}>{children}</FathomProvider>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const { result } = renderHook(() => useFathom(), { wrapper })
|
|
28
|
+
|
|
29
|
+
expect(result.current.client).toBe(mockClient)
|
|
30
|
+
expect(result.current.trackEvent).toBeDefined()
|
|
31
|
+
expect(result.current.trackPageview).toBeDefined()
|
|
32
|
+
expect(result.current.trackGoal).toBeDefined()
|
|
33
|
+
expect(result.current.load).toBeDefined()
|
|
34
|
+
expect(result.current.setSite).toBeDefined()
|
|
35
|
+
expect(result.current.blockTrackingForMe).toBeDefined()
|
|
36
|
+
expect(result.current.enableTrackingForMe).toBeDefined()
|
|
37
|
+
expect(result.current.isTrackingEnabled).toBeDefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should return empty context when used outside FathomProvider', () => {
|
|
41
|
+
const { result } = renderHook(() => useFathom())
|
|
42
|
+
|
|
43
|
+
expect(result.current.client).toBeUndefined()
|
|
44
|
+
expect(result.current.trackEvent).toBeUndefined()
|
|
45
|
+
expect(result.current.trackPageview).toBeUndefined()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should have displayName', () => {
|
|
49
|
+
expect(useFathom.displayName).toBe('useFathom')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import { FathomContext } from '../FathomContext'
|
|
4
|
+
import type { FathomContextInterface } from '../types'
|
|
5
|
+
|
|
6
|
+
export const useFathom = (): FathomContextInterface => {
|
|
7
|
+
const context = useContext(FathomContext)
|
|
8
|
+
return context
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
useFathom.displayName = 'useFathom'
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { renderHook, fireEvent, render } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { FathomProvider } from '../FathomProvider'
|
|
8
|
+
import { useTrackOnClick } from './useTrackOnClick'
|
|
9
|
+
|
|
10
|
+
describe('useTrackOnClick', () => {
|
|
11
|
+
const mockTrackEvent = vi.fn()
|
|
12
|
+
const mockClient = {
|
|
13
|
+
trackEvent: mockTrackEvent,
|
|
14
|
+
trackPageview: vi.fn(),
|
|
15
|
+
trackGoal: vi.fn(),
|
|
16
|
+
load: vi.fn(),
|
|
17
|
+
setSite: vi.fn(),
|
|
18
|
+
blockTrackingForMe: vi.fn(),
|
|
19
|
+
enableTrackingForMe: vi.fn(),
|
|
20
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
24
|
+
<FathomProvider client={mockClient}>{children}</FathomProvider>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should return a click handler function', () => {
|
|
32
|
+
const { result } = renderHook(
|
|
33
|
+
() => useTrackOnClick({ eventName: 'test-event' }),
|
|
34
|
+
{ wrapper },
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
expect(typeof result.current).toBe('function')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should track event when handler is called', () => {
|
|
41
|
+
const { result } = renderHook(
|
|
42
|
+
() => useTrackOnClick({ eventName: 'test-event' }),
|
|
43
|
+
{ wrapper },
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
result.current()
|
|
47
|
+
|
|
48
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1)
|
|
49
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('test-event', {})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should track event with options', () => {
|
|
53
|
+
const { result } = renderHook(
|
|
54
|
+
() =>
|
|
55
|
+
useTrackOnClick({
|
|
56
|
+
eventName: 'test-event',
|
|
57
|
+
id: 'test-id',
|
|
58
|
+
value: 100,
|
|
59
|
+
}),
|
|
60
|
+
{ wrapper },
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
result.current()
|
|
64
|
+
|
|
65
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('test-event', {
|
|
66
|
+
id: 'test-id',
|
|
67
|
+
value: 100,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should prevent default when preventDefault is true', () => {
|
|
72
|
+
const TestComponent = () => {
|
|
73
|
+
const handleClick = useTrackOnClick({
|
|
74
|
+
eventName: 'test-event',
|
|
75
|
+
preventDefault: true,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return <button onClick={handleClick}>Click me</button>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { getByText } = render(
|
|
82
|
+
<FathomProvider client={mockClient}>
|
|
83
|
+
<TestComponent />
|
|
84
|
+
</FathomProvider>,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const button = getByText('Click me')
|
|
88
|
+
const clickEvent = new MouseEvent('click', {
|
|
89
|
+
bubbles: true,
|
|
90
|
+
cancelable: true,
|
|
91
|
+
})
|
|
92
|
+
const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault')
|
|
93
|
+
|
|
94
|
+
fireEvent(button, clickEvent)
|
|
95
|
+
|
|
96
|
+
expect(preventDefaultSpy).toHaveBeenCalled()
|
|
97
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should not prevent default when preventDefault is false', () => {
|
|
101
|
+
const TestComponent = () => {
|
|
102
|
+
const handleClick = useTrackOnClick({
|
|
103
|
+
eventName: 'test-event',
|
|
104
|
+
preventDefault: false,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return <button onClick={handleClick}>Click me</button>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { getByText } = render(
|
|
111
|
+
<FathomProvider client={mockClient}>
|
|
112
|
+
<TestComponent />
|
|
113
|
+
</FathomProvider>,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const button = getByText('Click me')
|
|
117
|
+
const clickEvent = new MouseEvent('click', {
|
|
118
|
+
bubbles: true,
|
|
119
|
+
cancelable: true,
|
|
120
|
+
})
|
|
121
|
+
const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault')
|
|
122
|
+
|
|
123
|
+
fireEvent(button, clickEvent)
|
|
124
|
+
|
|
125
|
+
expect(preventDefaultSpy).not.toHaveBeenCalled()
|
|
126
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should call custom callback after tracking', () => {
|
|
130
|
+
const callback = vi.fn()
|
|
131
|
+
const { result } = renderHook(
|
|
132
|
+
() =>
|
|
133
|
+
useTrackOnClick({
|
|
134
|
+
eventName: 'test-event',
|
|
135
|
+
callback,
|
|
136
|
+
}),
|
|
137
|
+
{ wrapper },
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const mockEvent = {} as React.MouseEvent
|
|
141
|
+
result.current(mockEvent)
|
|
142
|
+
|
|
143
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1)
|
|
144
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
145
|
+
expect(callback).toHaveBeenCalledWith(mockEvent)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should call callback even when event is undefined', () => {
|
|
149
|
+
const callback = vi.fn()
|
|
150
|
+
const { result } = renderHook(
|
|
151
|
+
() =>
|
|
152
|
+
useTrackOnClick({
|
|
153
|
+
eventName: 'test-event',
|
|
154
|
+
callback,
|
|
155
|
+
}),
|
|
156
|
+
{ wrapper },
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
result.current()
|
|
160
|
+
|
|
161
|
+
expect(mockTrackEvent).toHaveBeenCalledTimes(1)
|
|
162
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
163
|
+
expect(callback).toHaveBeenCalledWith(undefined)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should memoize the handler function', () => {
|
|
167
|
+
// Note: Due to eventOptions being created from destructuring,
|
|
168
|
+
// the handler will be recreated if eventOptions object reference changes.
|
|
169
|
+
// This test verifies that the handler is stable when options don't change.
|
|
170
|
+
const options = { eventName: 'test-event' }
|
|
171
|
+
const { result } = renderHook(() => useTrackOnClick(options), { wrapper })
|
|
172
|
+
|
|
173
|
+
const firstHandler = result.current
|
|
174
|
+
// Call the handler to ensure it works
|
|
175
|
+
firstHandler()
|
|
176
|
+
|
|
177
|
+
expect(mockTrackEvent).toHaveBeenCalledWith('test-event', {})
|
|
178
|
+
// The handler should be a function
|
|
179
|
+
expect(typeof firstHandler).toBe('function')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should create new handler when options change', () => {
|
|
183
|
+
const { result, rerender } = renderHook(
|
|
184
|
+
({ eventName }) => useTrackOnClick({ eventName }),
|
|
185
|
+
{
|
|
186
|
+
wrapper,
|
|
187
|
+
initialProps: { eventName: 'event-1' },
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const firstHandler = result.current
|
|
192
|
+
rerender({ eventName: 'event-2' })
|
|
193
|
+
const secondHandler = result.current
|
|
194
|
+
|
|
195
|
+
expect(firstHandler).not.toBe(secondHandler)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import type { MouseEvent } from 'react'
|
|
3
|
+
|
|
4
|
+
import type { EventOptions } from 'fathom-client'
|
|
5
|
+
|
|
6
|
+
import { useFathom } from './useFathom'
|
|
7
|
+
|
|
8
|
+
export interface UseTrackOnClickOptions extends EventOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Event name to track
|
|
11
|
+
*/
|
|
12
|
+
eventName: string
|
|
13
|
+
/**
|
|
14
|
+
* Whether to prevent default behavior
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
preventDefault?: boolean
|
|
18
|
+
/**
|
|
19
|
+
* Optional callback function to run after tracking
|
|
20
|
+
* Receives the click event as a parameter
|
|
21
|
+
*/
|
|
22
|
+
callback?: (e?: MouseEvent) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook that returns a click handler function that tracks an event
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* function Button() {
|
|
31
|
+
* const handleClick = useTrackOnClick({
|
|
32
|
+
* eventName: 'button-click',
|
|
33
|
+
* id: 'signup-button',
|
|
34
|
+
* callback: (e) => {
|
|
35
|
+
* console.log('Button clicked!')
|
|
36
|
+
* // Your custom logic here
|
|
37
|
+
* },
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* return <button onClick={handleClick}>Sign Up</button>
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const useTrackOnClick = (
|
|
45
|
+
options: UseTrackOnClickOptions,
|
|
46
|
+
): ((e?: MouseEvent) => void) => {
|
|
47
|
+
const { trackEvent } = useFathom()
|
|
48
|
+
const {
|
|
49
|
+
eventName,
|
|
50
|
+
preventDefault = false,
|
|
51
|
+
callback,
|
|
52
|
+
...eventOptions
|
|
53
|
+
} = options
|
|
54
|
+
|
|
55
|
+
return useCallback(
|
|
56
|
+
(e?: MouseEvent) => {
|
|
57
|
+
if (preventDefault && e) {
|
|
58
|
+
e.preventDefault()
|
|
59
|
+
}
|
|
60
|
+
trackEvent?.(eventName, eventOptions)
|
|
61
|
+
callback?.(e)
|
|
62
|
+
},
|
|
63
|
+
[eventName, preventDefault, trackEvent, eventOptions, callback],
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { renderHook } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { FathomProvider } from '../FathomProvider'
|
|
8
|
+
import { useTrackOnMount } from './useTrackOnMount'
|
|
9
|
+
|
|
10
|
+
describe('useTrackOnMount', () => {
|
|
11
|
+
const mockTrackPageview = vi.fn()
|
|
12
|
+
const mockClient = {
|
|
13
|
+
trackEvent: vi.fn(),
|
|
14
|
+
trackPageview: mockTrackPageview,
|
|
15
|
+
trackGoal: vi.fn(),
|
|
16
|
+
load: vi.fn(),
|
|
17
|
+
setSite: vi.fn(),
|
|
18
|
+
blockTrackingForMe: vi.fn(),
|
|
19
|
+
enableTrackingForMe: vi.fn(),
|
|
20
|
+
isTrackingEnabled: vi.fn(() => true),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
24
|
+
<FathomProvider client={mockClient}>{children}</FathomProvider>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should track pageview on mount', () => {
|
|
32
|
+
renderHook(() => useTrackOnMount(), { wrapper })
|
|
33
|
+
|
|
34
|
+
expect(mockTrackPageview).toHaveBeenCalledTimes(1)
|
|
35
|
+
// The hook passes undefined, which becomes {} when merged
|
|
36
|
+
expect(mockTrackPageview).toHaveBeenCalled()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should track pageview with options', () => {
|
|
40
|
+
const options = { url: '/test-page' }
|
|
41
|
+
renderHook(() => useTrackOnMount(options), { wrapper })
|
|
42
|
+
|
|
43
|
+
expect(mockTrackPageview).toHaveBeenCalledTimes(1)
|
|
44
|
+
expect(mockTrackPageview).toHaveBeenCalledWith(options)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should only track once on mount', () => {
|
|
48
|
+
const { rerender } = renderHook(() => useTrackOnMount(), { wrapper })
|
|
49
|
+
|
|
50
|
+
expect(mockTrackPageview).toHaveBeenCalledTimes(1)
|
|
51
|
+
|
|
52
|
+
rerender()
|
|
53
|
+
expect(mockTrackPageview).toHaveBeenCalledTimes(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should not track when used outside FathomProvider', () => {
|
|
57
|
+
renderHook(() => useTrackOnMount())
|
|
58
|
+
|
|
59
|
+
expect(mockTrackPageview).not.toHaveBeenCalled()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should track with defaultPageviewOptions from provider', () => {
|
|
63
|
+
const defaultOptions = { url: '/default' }
|
|
64
|
+
const customWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
65
|
+
<FathomProvider
|
|
66
|
+
client={mockClient}
|
|
67
|
+
defaultPageviewOptions={defaultOptions}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</FathomProvider>
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
renderHook(() => useTrackOnMount(), { wrapper: customWrapper })
|
|
74
|
+
|
|
75
|
+
expect(mockTrackPageview).toHaveBeenCalledTimes(1)
|
|
76
|
+
// The hook passes undefined, provider merges defaults internally
|
|
77
|
+
expect(mockTrackPageview).toHaveBeenCalledWith({ url: '/default' })
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { PageViewOptions } from 'fathom-client'
|
|
4
|
+
|
|
5
|
+
import { useFathom } from './useFathom'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to track a pageview when a component mounts
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* function MyComponent() {
|
|
13
|
+
* useTrackOnMount({ url: '/custom-page' })
|
|
14
|
+
* return <div>Content</div>
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const useTrackOnMount = (options?: PageViewOptions) => {
|
|
19
|
+
const { trackPageview } = useFathom()
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
trackPageview?.(options)
|
|
23
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
24
|
+
}
|