Skip to content

Added custom createSnapshot support #291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/@posva/vuefire-core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function bindCollection (
added: ({ newIndex, doc }) => {
arraySubs.splice(newIndex, 0, Object.create(null))
const subs = arraySubs[newIndex]
const snapshot = createSnapshot(doc)
const snapshot = createSnapshot(options, doc)
const [data, refs] = extractRefs(snapshot)
// NOTE use ops
ops.add(array, newIndex, data)
Expand All @@ -118,7 +118,7 @@ export function bindCollection (
// NOTE use ops
const oldData = ops.remove(array, oldIndex)[0]
// const oldData = array.splice(oldIndex, 1)[0]
const snapshot = createSnapshot(doc)
const snapshot = createSnapshot(options, doc)
const [data, refs] = extractRefs(snapshot, oldData)
// NOTE use ops
ops.add(array, newIndex, data)
Expand Down Expand Up @@ -224,7 +224,7 @@ function subscribeToDocument (
if (doc.exists) {
updateDataFromDocumentSnapshot(
{
snapshot: createSnapshot(doc),
snapshot: createSnapshot(options, doc),
target,
path,
ops,
Expand Down Expand Up @@ -273,7 +273,7 @@ export function bindDocument (
if (doc.exists) {
updateDataFromDocumentSnapshot(
{
snapshot: createSnapshot(doc),
snapshot: createSnapshot(options, doc),
target: vm,
path: key,
subs,
Expand Down
16 changes: 13 additions & 3 deletions packages/@posva/vuefire-core/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@
* @param {firebase.firestore.DocumentSnapshot} doc
* @return {DocumentData}
*/
export function createSnapshot (doc) {
export function defaultCreateSnapshot (doc) {
// defaults everything to false, so no need to set
return Object.defineProperty(doc.data(), 'id', {
value: doc.id
})
}

/**
*
* @param {object} options
* @param {firebase.firestore.DocumentSnapshot} doc
* @returns {DocumentData}
*/
export function createSnapshot ({ createSnapshot }, doc) {
const possibleUserImplementation = createSnapshot || defaultCreateSnapshot
return possibleUserImplementation(doc)
}

/**
*
* @param {any} o
Expand All @@ -25,8 +36,7 @@ function isObject (o) {
/**
*
* @param {any} o
* should be o is Date https://github.com/Microsoft/TypeScript/issues/26297
* @returns {boolean}
* @returns {o is Date}
*/
function isTimestamp (o) {
return o.toDate
Expand Down
24 changes: 23 additions & 1 deletion packages/@posva/vuefire-core/test/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,29 @@ describe('utils', () => {
items: [{ text: 'foo' }],
ref: docRef
})
snapshot = createSnapshot(doc)
snapshot = createSnapshot({}, doc)
})

it('implements custom createSnapshot functions', () => {
const expectedObject = { foo: 'bar' }
let mockOptions = {
createSnapshot () {
return expectedObject
}
}
snapshot = createSnapshot(mockOptions, doc)
expect(snapshot).toStrictEqual(expectedObject)

mockOptions = {
createSnapshot (internalDoc) {
return Object.defineProperty(internalDoc.data(), 'customId', {
value: internalDoc.id
})
}
}

snapshot = createSnapshot(mockOptions, doc)
expect(snapshot.customId).toEqual(doc.id)
})

it('createSnapshot adds an id', () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/documentation/docs/api/vuefire.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,37 @@ Vue.use(firestorePlugin, options)

- `bindName`: name for the [`$bind`](#bind) method added to all Vue components. Defaults to `$bind`.
- `unbindName`: name for the [`$unbind`](#unbind) method added to all Vue components. Defaults to `$unbind`.
- `createSnapshot`: a function to provide a custom binding strategy when a document is received from firebase

`createSnapshot` is a function that receives a [DocumentSnapshot](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot) as argument and returns the object that is going to be bound to your component instance.

NOTE: when using the `createSnapshot` option you won't have the document `id` automatically bound to your data property. It's supposed to be used only for advanced cases, like adding `distance` to snapshots when using [Geofirestore](https://github.com/geofirestore/geofirestore-js/).

For instance:
```js
export default {
data () {
return {
todos: []
}
},
created () {
const options = {
createSnapshot: (documentSnapshot) => {
// the object below is going to be bound to todos.
// Since isTodo and customId aren't enumerable properties,
// they won't be passed when persisting data to firestore.
return Object.defineProperties(documentSnapshot.data(), {
isTodo: { value: true },
// mannually adding the id
customId: { value: documentSnapshot.id }
})
}
}
this.$bind('todos', db.collection('todos'), options)
}
}
```

## `firestore` option

Expand Down Expand Up @@ -120,6 +151,7 @@ Object that can contain the following properties:

- `maxRefDepth`: How many levels of nested references should be automatically bound. Defaults to 2, meaning that References inside of References inside of documents bound with `bindFirestoreRef` will automatically be bound too.
- `reset`: Allows to define the behavior when a reference is unbound. Defaults to `true`, which resets the property in the vue instance to `null` for documents and to an empty array `[]` for collections. It can also be set to a function returning a value to customize the value set. Setting it to `false` will keep the data as-is when unbounding.
- `createSnapshot`: A function that defines a custom snapshot binding strategy. It works just like the global createSnapshot in [firestorePlugin](#firestoreplugin) but only for the reference being bound.

## \$unbind

Expand Down
8 changes: 5 additions & 3 deletions packages/vuefire/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ function bind ({ vm, key, ref, ops }, options) {

export function firestorePlugin (
Vue,
{ bindName = '$bind', unbindName = '$unbind' } = {}
{ bindName = '$bind', unbindName = '$unbind', createSnapshot } = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the naming because a snapshot has a particular meaning in Firestore, so let's try to find other alternatives. Maybe serializeSnapshot?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe something with populate would be nice, since it's gonna get the firestore data and populate the vm.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this message, sorry :(
I think we should emphasize on the fact that it transforms the data coming from Firebase before we set it on the instance, that's why I think the verb should be related to that with something like serialize or serializer

) {
const strategies = Vue.config.optionMergeStrategies
strategies.firestore = strategies.provide

const globalMixinOptions = {
createSnapshot
}
Vue.mixin({
beforeCreate () {
this._firestoreUnbinds = Object.create(null)
Expand All @@ -57,7 +59,7 @@ export function firestorePlugin (
typeof firestore === 'function' ? firestore.call(this) : firestore
if (!refs) return
Object.keys(refs).forEach(key => {
this[bindName](key, refs[key])
this[bindName](key, refs[key], globalMixinOptions)
})
},

Expand Down
43 changes: 39 additions & 4 deletions packages/vuefire/test/options.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
import { firestorePlugin } from '../src'
import { Vue } from '@posva/vuefire-test-helpers'
import { db, delay, Vue } from '@posva/vuefire-test-helpers'

const createLocalVue = () => {
const newVue = Vue.extend()
newVue.config = Vue.config
return newVue
}

describe('Firestore: plugin options', () => {
it('allows customizing $rtdbBind', () => {
Vue.use(firestorePlugin, { bindName: '$myBind', unbindName: '$myUnbind' })
expect(typeof Vue.prototype.$myBind).toBe('function')
expect(typeof Vue.prototype.$myUnbind).toBe('function')
const LocalVue = createLocalVue()
LocalVue.use(firestorePlugin, { bindName: '$myBind', unbindName: '$myUnbind' })
expect(typeof LocalVue.prototype.$myBind).toBe('function')
expect(typeof LocalVue.prototype.$myUnbind).toBe('function')
})

it('allows global use of a custom createSnapshot function', async () => {
const LocalVue = createLocalVue()
const pluginOptions = {
createSnapshot: jest.fn((documentSnapshot) => {
return {
customId: documentSnapshot.id,
globalIsBar: documentSnapshot.data().foo === 'bar',
stuff: documentSnapshot.data()
}
})
}
LocalVue.use(firestorePlugin, pluginOptions)

const items = db.collection()
const item = items.doc()
const itemMock = { foo: 'bar' }
await item.set(itemMock)

const vm = new LocalVue({
data: () => ({ items: [] }),
firestore: { items }
})
await delay(5)
expect(pluginOptions.createSnapshot).toHaveBeenCalledTimes(1)
expect(Array.isArray(vm.items)).toBe(true)
expect(vm.items[0]).toEqual({ customId: '0', globalIsBar: true, stuff: itemMock })
})
})
12 changes: 10 additions & 2 deletions packages/vuefire/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,13 @@ interface Options {
unbindName?: string
}

export declare const firestorePlugin: PluginFunction<Options>
export declare const rtdbPlugin: PluginFunction<Options>
interface FirestoreOptions extends Options {
createSnapshot?: (documentSnapshot: firebase.firestore.DocumentSnapshot) => Record<string, any>
}

interface RTDBOptions extends Options {
createSnapshot?: (documentSnapshot: firebase.database.DataSnapshot) => Record<string, any>
}

export declare const firestorePlugin: PluginFunction<FirestoreOptions>
export declare const rtdbPlugin: PluginFunction<RTDBOptions>
6 changes: 6 additions & 0 deletions packages/vuefire/types/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ app.$bind('collection', db.collection('todos')).then(todos => {
todos.length
})

app.$bind('collection', db.collection('todos'), {
createSnapshot (snapshot) {
return { exists: snapshot.exists, ...snapshot.data() }
}
})

app.$bind('todos', todosSorted).then(todos => {
todos.length
})
Expand Down
2 changes: 2 additions & 0 deletions packages/vuefire/types/vue.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { firestore, database } from 'firebase'
export interface BindFirestoreRefOptions {
maxRefDepth?: number
reset?: boolean | (() => any)
createSnapshot?: (documentSnapshot: firestore.DocumentSnapshot) => Record<string, any>
}

export interface BindRTDBRefOptions {
reset?: boolean | (() => any)
createSnapshot?: (dataSnapshot: database.DataSnapshot) => Record<string, any>
}

declare module 'vue/types/vue' {
Expand Down