Skip to content

Commit 1c546a5

Browse files
authored
Merge pull request #5 from microcipcip/feature/useGeolocation
Feature/use geolocation
2 parents dc49c36 + f1bfcd9 commit 1c546a5

File tree

12 files changed

+336
-5
lines changed

12 files changed

+336
-5
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@ Vue.use(VueCompositionAPI);
5959
## APIs
6060

6161
- Sensors
62+
- [`useGeolocation`](./src/components/useGeolocation/stories/useGeolocation.md) — tracks geolocation state of user's device.
63+
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usegeolocation--demo)
6264
- [`useHover`](./src/components/useHover/stories/useHover.md) — tracks mouse hover state of a given element.
6365
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usehover--demo)
64-
- [`useIntersection`](./src/components/useIntersection/stories/useIntersection.md) — tracks intersection of target element with an ancestor element.
66+
- [`useIntersection`](./src/components/useIntersection/stories/useIntersection.md) — tracks intersection of target element with an ancestor element.
6567
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useintersection--demo)
6668
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useintersection--advanced-demo)
6769
- [`useMedia`](./src/components/useMedia/stories/useMedia.md) — tracks state of a CSS media query.

src/components/useFullscreen/useFullscreen.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const testComponent = () => ({
2121
})
2222

2323
describe('useFullscreen', () => {
24-
it('should not be fullscreen when initialized', () => {
24+
it('should not be fullscreen onMounted', () => {
2525
const wrapper = mount(testComponent())
2626
expect(wrapper.find('#isFullscreen').exists()).toBe(false)
2727
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useGeolocation'
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<template>
2+
<table class="table is-fullwidth">
3+
<thead>
4+
<tr>
5+
<th>Prop</th>
6+
<th>Value</th>
7+
</tr>
8+
</thead>
9+
<tbody>
10+
<tr>
11+
<td>geo</td>
12+
<td>
13+
<pre>{{ JSON.stringify(geo, null, 2) }}</pre>
14+
</td>
15+
</tr>
16+
<tr>
17+
<td colspan="2">
18+
<button class="button is-primary" @click="start" v-if="!isTracking">
19+
Enable geolocation tracking
20+
</button>
21+
<button class="button is-danger" @click="stop" v-else>
22+
Disable geolocation tracking
23+
</button>
24+
</td>
25+
</tr>
26+
</tbody>
27+
</table>
28+
</template>
29+
30+
<script lang="ts">
31+
import Vue from 'vue'
32+
import { useGeolocation } from '@src/vue-use-kit'
33+
34+
export default Vue.extend({
35+
name: 'UseGeolocationDemo',
36+
setup() {
37+
const { isTracking, geo, start, stop } = useGeolocation({}, false)
38+
return { isTracking, geo, start, stop }
39+
}
40+
})
41+
</script>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# useGeolocation
2+
3+
Vue function that tracks geolocation state of user's device, based on the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).
4+
5+
## Reference
6+
7+
```typescript
8+
interface UseGeolocation {
9+
loading: boolean
10+
accuracy: number | null
11+
altitude: number | null
12+
altitudeAccuracy: number | null
13+
heading: number | null
14+
latitude: number | null
15+
longitude: number | null
16+
speed: number | null
17+
timestamp: number | null
18+
error?: Error | PositionError
19+
}
20+
```
21+
22+
```typescript
23+
useGeolocation(
24+
options?: PositionOptions,
25+
runOnMount?: boolean
26+
): {
27+
isTracking: Ref<boolean>;
28+
geo: Ref<UseGeolocation>;
29+
start: () => void;
30+
stop: () => void;
31+
}
32+
```
33+
34+
### Parameters
35+
36+
- `options: PositionOptions` the [geolocation position options](https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions)
37+
- `runOnMount: boolean` whether to run the geolocation tracking on mount, `true` by default
38+
39+
### Returns
40+
41+
- `isTracking: Ref<boolean>` whether the function is tracking the user's location or not
42+
- `geo: Ref<UseGeolocation>` the geolocation object
43+
- `start: Function` the function used for starting the geolocation tracking
44+
- `stop: Function` the function used for stopping the geolocation tracking
45+
46+
## Usage
47+
48+
```html
49+
<template>
50+
<div>
51+
<div>
52+
Geo:
53+
<pre>{{ JSON.stringify(geo, null, 2) }}</pre>
54+
</div>
55+
<button @click="start" v-if="!isTracking">
56+
Enable geolocation tracking
57+
</button>
58+
<button @click="stop" v-else>Disable geolocation tracking</button>
59+
</div>
60+
</template>
61+
62+
<script lang="ts">
63+
import Vue from 'vue'
64+
import { useGeolocation } from 'vue-use-kit'
65+
66+
export default Vue.extend({
67+
name: 'UseGeolocationDemo',
68+
setup() {
69+
const { isTracking, geo, start, stop } = useGeolocation()
70+
return { isTracking, geo, start, stop }
71+
}
72+
})
73+
</script>
74+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { storiesOf } from '@storybook/vue'
2+
import path from 'path'
3+
import StoryTitle from '@src/helpers/StoryTitle.vue'
4+
import UseGeolocationDemo from './UseGeolocationDemo.vue'
5+
6+
const functionName = 'useGeolocation'
7+
const functionPath = path.resolve(__dirname, '..')
8+
const notes = require(`./${functionName}.md`).default
9+
10+
const basicDemo = () => ({
11+
components: { StoryTitle, demo: UseGeolocationDemo },
12+
template: `
13+
<div class="container">
14+
<story-title
15+
function-path="${functionPath}"
16+
source-name="${functionName}"
17+
demo-name="UseGeolocationDemo.vue"
18+
>
19+
<template v-slot:title></template>
20+
<template v-slot:intro></template>
21+
</story-title>
22+
<demo />
23+
</div>`
24+
})
25+
26+
storiesOf('sensors|useGeolocation', module)
27+
.addParameters({ notes })
28+
.add('Demo', basicDemo)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { mount } from '@src/helpers/test'
2+
import { useGeolocation } from '@src/vue-use-kit'
3+
4+
let watchPosition: any
5+
let clearWatch: any
6+
let getCurrentPosition: any
7+
8+
beforeEach(() => {
9+
watchPosition = jest.fn()
10+
clearWatch = jest.fn()
11+
getCurrentPosition = jest.fn().mockImplementationOnce(success =>
12+
Promise.resolve(
13+
success({
14+
coords: {
15+
latitude: 51.1,
16+
longitude: 45.3
17+
}
18+
})
19+
)
20+
)
21+
const inst = ((navigator as any).geolocation = {
22+
watchPosition,
23+
clearWatch,
24+
getCurrentPosition
25+
})
26+
})
27+
28+
afterEach(() => {
29+
jest.clearAllMocks()
30+
})
31+
32+
const testComponent = () => ({
33+
template: `
34+
<div>
35+
<div id="isTracking" v-if="isTracking"></div>
36+
<button id="start" @click="start"></button>
37+
<button id="stop" @click="stop"></button>
38+
</div>
39+
`,
40+
setup() {
41+
const { isTracking, geo, start, stop } = useGeolocation()
42+
return { isTracking, geo, start, stop }
43+
}
44+
})
45+
46+
describe('useGeolocation', () => {
47+
it('should call getCurrentPosition and watchPosition onMounted', () => {
48+
expect(getCurrentPosition).toHaveBeenCalledTimes(0)
49+
expect(watchPosition).toHaveBeenCalledTimes(0)
50+
mount(testComponent())
51+
expect(getCurrentPosition).toHaveBeenCalledTimes(1)
52+
expect(watchPosition).toHaveBeenCalledTimes(1)
53+
})
54+
55+
it('should call clearWatch onUnmounted', () => {
56+
expect(clearWatch).toHaveBeenCalledTimes(0)
57+
const wrapper = mount(testComponent())
58+
wrapper.vm.$destroy()
59+
expect(clearWatch).toHaveBeenCalledTimes(1)
60+
})
61+
62+
it('should call getCurrentPosition again when start is called', async () => {
63+
expect(getCurrentPosition).toHaveBeenCalledTimes(0)
64+
const wrapper = mount(testComponent())
65+
expect(getCurrentPosition).toHaveBeenCalledTimes(1)
66+
wrapper.find('#stop').trigger('click')
67+
68+
// Wait for Vue to append #start in the DOM
69+
await wrapper.vm.$nextTick()
70+
wrapper.find('#start').trigger('click')
71+
expect(getCurrentPosition).toHaveBeenCalledTimes(2)
72+
})
73+
74+
it('should call clearWatch when stop is called', async () => {
75+
expect(clearWatch).toHaveBeenCalledTimes(0)
76+
const wrapper = mount(testComponent())
77+
wrapper.find('#stop').trigger('click')
78+
79+
// Wait for Vue to append #start in the DOM
80+
await wrapper.vm.$nextTick()
81+
expect(clearWatch).toHaveBeenCalledTimes(1)
82+
})
83+
})
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { ref, onMounted, onUnmounted, Ref } from '@src/api'
2+
3+
export interface UseGeolocation {
4+
loading: boolean
5+
accuracy: number | null
6+
altitude: number | null
7+
altitudeAccuracy: number | null
8+
heading: number | null
9+
latitude: number | null
10+
longitude: number | null
11+
speed: number | null
12+
timestamp: number | null
13+
error?: Error | PositionError
14+
}
15+
16+
const defaultOpts = {
17+
enableHighAccuracy: false,
18+
timeout: Infinity,
19+
maximumAge: 0
20+
}
21+
22+
export function useGeolocation(
23+
options: PositionOptions = {},
24+
runOnMount = true
25+
) {
26+
options = Object.assign(defaultOpts, options)
27+
28+
// Note: surprisingly the watchId can be 0 (not positive) so
29+
// we have to check if watchId !== null every time
30+
let watchId: number | null = null
31+
32+
const geoInitData = {
33+
loading: false,
34+
accuracy: null,
35+
altitude: null,
36+
altitudeAccuracy: null,
37+
heading: null,
38+
latitude: null,
39+
longitude: null,
40+
speed: null,
41+
timestamp: null
42+
}
43+
44+
const isTracking = ref(false)
45+
const geo: Ref<UseGeolocation> = ref({ ...geoInitData })
46+
47+
const onEventError = (error: PositionError) => {
48+
if (watchId === null) return
49+
geo.value.loading = false
50+
geo.value.error = {
51+
code: error.code,
52+
message: error.message
53+
} as PositionError
54+
isTracking.value = false
55+
}
56+
57+
const handleGeolocation = ({ coords, timestamp }: Position) => {
58+
geo.value = {
59+
loading: false,
60+
accuracy: coords.accuracy,
61+
altitude: coords.altitude,
62+
altitudeAccuracy: coords.altitudeAccuracy,
63+
heading: coords.heading,
64+
latitude: coords.latitude,
65+
longitude: coords.longitude,
66+
speed: coords.speed,
67+
timestamp
68+
}
69+
isTracking.value = true
70+
}
71+
72+
const start = () => {
73+
if (watchId !== null) return
74+
geo.value.loading = true
75+
geo.value.timestamp = Date.now()
76+
77+
navigator.geolocation.getCurrentPosition(
78+
handleGeolocation,
79+
onEventError,
80+
options
81+
)
82+
83+
watchId = navigator.geolocation.watchPosition(
84+
handleGeolocation,
85+
onEventError,
86+
options
87+
)
88+
}
89+
90+
const stop = () => {
91+
if (watchId === null) return
92+
navigator.geolocation.clearWatch(watchId)
93+
watchId = null
94+
isTracking.value = false
95+
}
96+
97+
onMounted(() => runOnMount && start())
98+
onUnmounted(stop)
99+
100+
return { isTracking, geo, start, stop }
101+
}

src/components/useIntersection/useIntersection.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const testComponent = (onMount = true) => ({
3131
})
3232

3333
describe('useIntersection', () => {
34-
it('should call IntersectionObserver when initialized', () => {
34+
it('should call IntersectionObserver onMounted', () => {
3535
expect(observe).toHaveBeenCalledTimes(0)
3636
mount(testComponent())
3737
expect(observe).toHaveBeenCalledTimes(1)

src/components/useIntervalFn/useIntervalFn.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const testComponent = (onMount = true) => ({
3333
})
3434

3535
describe('useIntervalFn', () => {
36-
it('should call setInterval when initialized', () => {
36+
it('should call setInterval onMounted', () => {
3737
expect(setInterval).toHaveBeenCalledTimes(0)
3838
mount(testComponent())
3939
jest.advanceTimersByTime(1500)

0 commit comments

Comments
 (0)