Skip to content

Commit 7af2c6e

Browse files
committed
feat(firestore): allow setting the ref value to null
1 parent f53144f commit 7af2c6e

File tree

5 files changed

+160
-28
lines changed

5 files changed

+160
-28
lines changed

src/firestore/index.ts

+32-14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
Query,
55
FirestoreError,
66
DocumentData,
7+
FirestoreDataConverter,
78
} from 'firebase/firestore'
89
import {
910
getCurrentScope,
@@ -23,6 +24,7 @@ import {
2324
ResetOption,
2425
walkSet,
2526
_MaybeRef,
27+
_Nullable,
2628
_RefWithState,
2729
} from '../shared'
2830
import { addPendingPromise } from '../ssr/plugin'
@@ -56,11 +58,13 @@ export interface _UseFirestoreRefOptions extends FirestoreRefOptions {
5658
*/
5759
export function _useFirestoreRef(
5860
docOrCollectionRef: _MaybeRef<
59-
DocumentReference<unknown> | Query<unknown> | CollectionReference<unknown>
61+
_Nullable<
62+
DocumentReference<unknown> | Query<unknown> | CollectionReference<unknown>
63+
>
6064
>,
6165
localOptions?: _UseFirestoreRefOptions
6266
) {
63-
let _unbind!: UnbindType
67+
let _unbind: UnbindType = noop
6468
const options = Object.assign({}, firestoreOptions, localOptions)
6569

6670
// TODO: allow passing pending and error refs as option for when this is called using the options api
@@ -74,8 +78,19 @@ export function _useFirestoreRef(
7478
let removePendingPromise = noop
7579

7680
function bindFirestoreRef() {
81+
let docRefValue = unref(docOrCollectionRef)
82+
7783
const p = new Promise<unknown | null>((resolve, reject) => {
78-
let docRefValue = unref(docOrCollectionRef)
84+
// stop the previous subscription
85+
_unbind(options.reset)
86+
// skip if the ref is null or undefined
87+
// we still want to create the new promise
88+
if (!docRefValue) {
89+
_unbind = noop
90+
// TODO: maybe we shouldn't resolve this at all?
91+
return resolve(null)
92+
}
93+
7994
if (!docRefValue.converter) {
8095
docRefValue = docRefValue.withConverter(
8196
// @ts-expect-error: seems like a ts error
@@ -95,9 +110,9 @@ export function _useFirestoreRef(
95110
})
96111

97112
// only add the first promise to the pending ones
98-
if (!isPromiseAdded) {
113+
if (!isPromiseAdded && docRefValue) {
99114
// TODO: is there a way to make this only for the first render?
100-
removePendingPromise = addPendingPromise(p, unref(docOrCollectionRef))
115+
removePendingPromise = addPendingPromise(p, docRefValue)
101116
isPromiseAdded = true
102117
}
103118
promise.value = p
@@ -173,7 +188,7 @@ export function useCollection<
173188
R extends CollectionReference<unknown> | Query<unknown>
174189
>(
175190
// TODO: add MaybeRef
176-
collectionRef: _MaybeRef<R>,
191+
collectionRef: _MaybeRef<_Nullable<R>>,
177192
options?: UseCollectionOptions
178193
): _RefFirestore<_InferReferenceType<R>[]>
179194

@@ -186,17 +201,20 @@ export function useCollection<
186201
* @param options - optional options
187202
*/
188203
export function useCollection<T>(
189-
collectionRef: _MaybeRef<CollectionReference | Query>,
204+
collectionRef: _MaybeRef<_Nullable<CollectionReference | Query>>,
190205
options?: UseCollectionOptions
191206
): _RefFirestore<VueFirestoreQueryData<T>>
192207

193208
export function useCollection<T>(
194-
collectionRef: _MaybeRef<CollectionReference<unknown> | Query<unknown>>,
209+
collectionRef: _MaybeRef<
210+
_Nullable<CollectionReference<unknown> | Query<unknown>>
211+
>,
195212
options?: UseCollectionOptions
196213
): _RefFirestore<VueFirestoreQueryData<T>> {
197-
return _useFirestoreRef(collectionRef, options) as _RefFirestore<
198-
VueFirestoreQueryData<T>
199-
>
214+
return _useFirestoreRef(collectionRef, {
215+
target: ref([]),
216+
...options,
217+
}) as _RefFirestore<VueFirestoreQueryData<T>>
200218
}
201219

202220
// TODO: split document and collection into two different parts
@@ -213,7 +231,7 @@ export function useDocument<
213231
// explicit generic as unknown to allow arbitrary types like numbers or strings
214232
R extends DocumentReference<unknown>
215233
>(
216-
documentRef: _MaybeRef<R>,
234+
documentRef: _MaybeRef<_Nullable<R>>,
217235
options?: UseDocumentOptions
218236
): _RefFirestore<_InferReferenceType<R>> // this one can't be null or should be specified in the converter
219237

@@ -226,12 +244,12 @@ export function useDocument<
226244
* @param options - optional options
227245
*/
228246
export function useDocument<T>(
229-
documentRef: _MaybeRef<DocumentReference>,
247+
documentRef: _MaybeRef<_Nullable<DocumentReference>>,
230248
options?: UseDocumentOptions
231249
): _RefFirestore<VueFirestoreDocumentData<T>>
232250

233251
export function useDocument<T>(
234-
documentRef: _MaybeRef<DocumentReference<unknown>>,
252+
documentRef: _MaybeRef<_Nullable<DocumentReference<unknown>>>,
235253
options?: UseDocumentOptions
236254
):
237255
| _RefFirestore<VueFirestoreDocumentData<T> | null>

src/firestore/subscribe.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
_MaybeRef,
77
ResetOption,
88
_DataSourceOptions,
9+
noop,
910
} from '../shared'
1011
import { ref, Ref, unref } from 'vue-demi'
1112
import type {
@@ -298,7 +299,7 @@ export function bindCollection<T = unknown>(
298299
},
299300
}
300301

301-
const unbind = onSnapshot(
302+
const stopOnSnapshot = onSnapshot(
302303
collection,
303304
(snapshot) => {
304305
// console.log('pending', metadata.hasPendingWrites)
@@ -330,7 +331,7 @@ export function bindCollection<T = unknown>(
330331
}
331332
originalResolve(unref(arrayRef))
332333
// reset resolve to noop
333-
resolve = () => {}
334+
resolve = noop
334335
}
335336
}
336337
}
@@ -355,7 +356,7 @@ export function bindCollection<T = unknown>(
355356
)
356357

357358
return (reset?: FirestoreRefOptions['reset']) => {
358-
unbind()
359+
stopOnSnapshot()
359360
if (reset !== false) {
360361
const value = typeof reset === 'function' ? reset() : []
361362
ops.set(target, key, value)
@@ -390,7 +391,7 @@ export function bindDocument<T>(
390391
// bind here the function so it can be resolved anywhere
391392
// this is specially useful for refs
392393
resolve = callOnceWithArg(resolve, () => walkGet(target, key))
393-
const _unbind = onSnapshot(
394+
const stopOnSnapshot = onSnapshot(
394395
document,
395396
(snapshot) => {
396397
if (snapshot.exists()) {
@@ -413,7 +414,7 @@ export function bindDocument<T>(
413414
)
414415

415416
return (reset?: FirestoreRefOptions['reset']) => {
416-
_unbind()
417+
stopOnSnapshot()
417418
if (reset !== false) {
418419
const value = typeof reset === 'function' ? reset() : null
419420
ops.set(target, key, value)

src/shared.ts

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export interface OperationsType {
3030
*/
3131
export type ResetOption = boolean | (() => unknown)
3232

33+
/**
34+
* @internal
35+
*/
36+
export type _Nullable<T> = T | null | undefined
37+
3338
export type TODO = any
3439
/**
3540
* Walks a path inside an object

tests/firestore/collection.spec.ts

+49-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
UseCollectionOptions,
1717
VueFirestoreQueryData,
1818
} from '../../src'
19-
import { _MaybeRef } from '../../src/shared'
19+
import { _MaybeRef, _Nullable } from '../../src/shared'
2020

2121
describe(
2222
'Firestore collections',
@@ -29,7 +29,7 @@ describe(
2929
ref = collection(),
3030
}: {
3131
options?: UseCollectionOptions
32-
ref?: _MaybeRef<CollectionReference<T>>
32+
ref?: _MaybeRef<_Nullable<CollectionReference<T>>>
3333
} = {}) {
3434
let data!: _RefFirestore<VueFirestoreQueryData<T>>
3535

@@ -47,7 +47,8 @@ describe(
4747

4848
return {
4949
wrapper,
50-
listRef: unref(ref),
50+
// to simplify types
51+
listRef: unref(ref)!,
5152
// non enumerable properties cannot be spread
5253
data: data.data,
5354
pending: data.pending,
@@ -62,7 +63,7 @@ describe(
6263
ref,
6364
}: {
6465
options?: UseCollectionOptions
65-
ref?: _MaybeRef<CollectionReference<T> | Query<T>>
66+
ref?: _MaybeRef<_Nullable<CollectionReference<T> | Query<T>>>
6667
} = {}) {
6768
let data!: _RefFirestore<VueFirestoreQueryData<T>>
6869

@@ -71,7 +72,7 @@ describe(
7172
setup() {
7273
// @ts-expect-error: generic forced
7374
data = useCollection(
74-
// @ts-expect-error: generic forced
75+
// split for ts
7576
ref,
7677
options
7778
)
@@ -263,19 +264,35 @@ describe(
263264
expect(data.value).toEqual([{ name: 'a' }])
264265
})
265266

266-
it('can be bound to a ref of a query', async () => {
267+
async function createFilteredLists() {
267268
const listRef = collection<{ text: string; finished: boolean }>()
268269
const finishedListRef = query(listRef, where('finished', '==', true))
269270
const unfinishedListRef = query(listRef, where('finished', '==', false))
270-
const showFinished = ref(false)
271+
const showFinished = ref<boolean | null>(false)
271272
const listToDisplay = computed(() =>
272-
showFinished.value ? finishedListRef : unfinishedListRef
273+
showFinished.value
274+
? finishedListRef
275+
: showFinished.value === false
276+
? unfinishedListRef
277+
: null
273278
)
274279
await addDoc(listRef, { text: 'task 1', finished: false })
275280
await addDoc(listRef, { text: 'task 2', finished: false })
276281
await addDoc(listRef, { text: 'task 3', finished: true })
277282
await addDoc(listRef, { text: 'task 4', finished: false })
278283

284+
return {
285+
listRef,
286+
finishedListRef,
287+
unfinishedListRef,
288+
showFinished,
289+
listToDisplay,
290+
}
291+
}
292+
293+
it('can be bound to a ref of a query', async () => {
294+
const { showFinished, listToDisplay } = await createFilteredLists()
295+
279296
const { wrapper, data, promise } = factoryQuery({
280297
ref: listToDisplay,
281298
})
@@ -294,6 +311,30 @@ describe(
294311
expect(data.value).toContainEqual({ text: 'task 3', finished: true })
295312
})
296313

314+
it('can be bound to a null ref', async () => {
315+
const { showFinished, listToDisplay } = await createFilteredLists()
316+
showFinished.value = null
317+
318+
const { data, promise } = factory({
319+
// @ts-expect-error
320+
ref: listToDisplay,
321+
})
322+
await promise.value
323+
324+
expect(data.value).toHaveLength(0)
325+
326+
showFinished.value = false
327+
await nextTick()
328+
await promise.value
329+
expect(data.value).toHaveLength(3)
330+
331+
showFinished.value = null
332+
await nextTick()
333+
await promise.value
334+
// it stays the same
335+
expect(data.value).toHaveLength(3)
336+
})
337+
297338
tds(() => {
298339
interface TodoI {
299340
text: string

tests/firestore/document.spec.ts

+68-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
FirestoreError,
88
} from 'firebase/firestore'
99
import { expectType, setupFirestoreRefs, tds, firestore } from '../utils'
10-
import { unref, type Ref } from 'vue'
10+
import { nextTick, shallowRef, unref, type Ref } from 'vue'
1111
import { _MaybeRef } from '../../src/shared'
1212
import {
1313
useDocument,
@@ -153,6 +153,73 @@ describe(
153153
})
154154
})
155155

156+
it('can be bound to a ref of a document', async () => {
157+
const aRef = doc()
158+
const bRef = doc()
159+
await setDoc(aRef, { name: 'a' })
160+
await setDoc(bRef, { name: 'b' })
161+
const targetRef = shallowRef(bRef)
162+
163+
const { data, promise } = factory({ ref: targetRef })
164+
await promise.value
165+
166+
expect(data.value).toEqual({ name: 'b' })
167+
168+
targetRef.value = aRef
169+
await nextTick()
170+
await promise.value
171+
expect(data.value).toEqual({ name: 'a' })
172+
})
173+
174+
it('can be bound to a null ref', async () => {
175+
const aRef = doc()
176+
const bRef = doc()
177+
await setDoc(aRef, { name: 'a' })
178+
await setDoc(bRef, { name: 'b' })
179+
const targetRef = shallowRef()
180+
181+
const { data, promise } = factory({ ref: targetRef })
182+
await promise.value
183+
184+
expect(data.value).toBeFalsy()
185+
186+
targetRef.value = aRef
187+
expect(data.value).toBeFalsy()
188+
await nextTick()
189+
await promise.value
190+
expect(data.value).toEqual({ name: 'a' })
191+
192+
targetRef.value = null
193+
await nextTick()
194+
await promise.value
195+
// it stays the same
196+
expect(data.value).toEqual({ name: 'a' })
197+
198+
targetRef.value = bRef
199+
await nextTick()
200+
await promise.value
201+
// it stays the same
202+
expect(data.value).toEqual({ name: 'b' })
203+
})
204+
205+
it('can be set to a null ref', async () => {
206+
const aRef = doc()
207+
const bRef = doc()
208+
await setDoc(aRef, { name: 'a' })
209+
await setDoc(bRef, { name: 'b' })
210+
const targetRef = shallowRef()
211+
212+
const { data, promise } = factory({ ref: targetRef })
213+
await promise.value
214+
215+
expect(data.value).toBeFalsy()
216+
217+
targetRef.value = aRef
218+
await nextTick()
219+
await promise.value
220+
expect(data.value).toEqual({ name: 'a' })
221+
})
222+
156223
tds(() => {
157224
const db = firestore
158225
const doc = originalDoc

0 commit comments

Comments
 (0)