Skip to content

Instantly share code, notes, and snippets.

@jdkanani
Last active January 10, 2019 10:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdkanani/7491954b5f4cd8ff62309be1f42ebf22 to your computer and use it in GitHub Desktop.
Save jdkanani/7491954b5f4cd8ff62309be1f42ebf22 to your computer and use it in GitHub Desktop.
Mobx firebase binding
import { extendObservable } from "mobx"
import { firebaseActions } from "../plugins/mobfire"
export default class FireModel {
// constructor
constructor(data = {}) {
// treat data as properties
const v = {}
Object.keys(data).forEach(key => {
if (key !== ".value" && key !== ".key" && key !== "_id") {
v[key] = data[key]
} else {
Object.defineProperty(this, key, {
enumerable: false,
configurable: false,
writable: false,
value: data[key]
})
}
})
extendObservable(this, v)
}
}
Object.assign(FireModel.prototype, firebaseActions)
import firebase from "firebase"
import { render } from "react-dom"
import TodoListModel from "./models/todolist-model"
import TodoModel from "./models/todo-model"
// initialize firebase
const store = new TodoListModel()
const source = firestore.collection("todos")
store.bindFirebaseStoreRef({
key: "todos",
source: source,
options: {
// once: true, // fetch only once
model: TodoModel
}
})
render(
<div>
<TodoList store={store} />
</div>,
document.getElementById("root")
)
// playing around in the console
window.store = store
/* eslint-disable */
// mutations
const MOBXFIRE_VALUE = "fire:onValue"
const MOBXFIRE_ARRAY_ADD = "fire:onChildAdded"
const MOBXFIRE_ARRAY_CHANGE = "fire:onChildChanged"
const MOBXFIRE_ARRAY_MOVE = "fire:onChildMoved"
const MOBXFIRE_ARRAY_REMOVE = "fire:onChildRemoved"
// check if object
const isObject = val =>
Object.prototype.toString.call(val) === "[object Object]"
const normalizeMap = map =>
Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
// Get key and ref
const getKey = snapshot =>
typeof snapshot.key === "function" ? snapshot.key() : snapshot.key
const getRef = refOrQuery => {
let result
if (typeof refOrQuery.ref === "function") {
result = refOrQuery.ref()
} else if (typeof refOrQuery.ref === "object") {
result = refOrQuery.ref
}
return result
}
// create record
const createRecord = (snapshot, Model, options = {}) => {
const value = snapshot.val()
let v = value
if (options.processValue) {
v = options.processValue(value) || value
}
let result
if (isObject(v)) {
result = Model && !(v instanceof Model) ? new Model(v) : v
} else {
result = { ".value": v }
}
result[".key"] = getKey(snapshot)
return result
}
// create document
const createDocument = (snapshot, Model, options = {}) => {
const value = snapshot.data()
value._id = snapshot.id
let v = value
if (options.processValue) {
v = options.processValue(value) || value
}
return Model && !(v instanceof Model) ? new Model(v) : v
}
// get index for key
const indexForKey = (array, key) => {
let i
for (i = 0; i < array.length; i += 1) {
if (array[i][".key"] === key) {
return i
}
}
return -1
}
export const bind = (vm, key, source, options = {}) => {
if (!vm.state.$firebaseRefs) {
vm.state.$firebaseRefs = Object.create(null)
vm.state.$firebaseSources = Object.create(null)
vm.state.$firebaseListeners = Object.create(null)
vm.state.$firebaseUnsubscribe = Object.create(null)
}
const asObject = !!options.asObject
const ref = getRef(source)
vm.state.$firebaseRefs[key] = ref
vm.state.$firebaseSources[key] = source
// bind based on initial value type
if (asObject) {
bindAsObject(vm, key, source, options)
} else {
bindAsArray(vm, key, source, options)
}
}
export const bindStore = (vm, key, source, options = {}) => {
if (!vm.state.$firebaseRefs) {
vm.state.$firebaseRefs = Object.create(null)
vm.state.$firebaseSources = Object.create(null)
vm.state.$firebaseListeners = Object.create(null)
vm.state.$firebaseUnsubscribe = Object.create(null)
}
const asDocument = !!options.asDocument
const ref = source
vm.state.$firebaseRefs[key] = ref
vm.state.$firebaseSources[key] = source
// bind based on initial value type
if (asDocument) {
return bindAsDocument(vm, key, source, options)
}
return bindAsCollection(vm, key, source, options)
}
export const unbind = (vm, key) => {
const source = vm.state.$firebaseSources && vm.state.$firebaseSources[key]
if (!source) {
throw new Error(
`MobxFire: unbind failed: ${key} is not bound to a Firebase reference.`
)
}
const listeners = vm.state.$firebaseListeners[key]
Object.keys(listeners).forEach(event => {
source.off(event, listeners[event])
})
const unsubscribe = vm.state.$firebaseUnsubscribe[key]
if (unsubscribe) {
try {
unsubscribe()
} catch (e) {}
}
// set value to mobx or simple component
firebaseMutations[MOBXFIRE_VALUE](vm.state, { key, value: null })
vm.state.$firebaseRefs[key] = null
vm.state.$firebaseSources[key] = null
vm.state.$firebaseListeners[key] = null
vm.state.$firebaseUnsubscribe[key] = null
}
const bindAsObject = (vm, key, source, options) => {
let func = options.once ? source.once : source.on
func = func.bind(source) // need to bind with source object
const cb = func(
"value",
snapshot => {
const value = createRecord(snapshot, options.model, options)
// set value
firebaseMutations[MOBXFIRE_VALUE](vm.state, { key, value })
// on change callback
options.onChange && options.onChange(value)
},
options.cancelCallback
)
vm.state.$firebaseListeners[key] = { value: cb }
}
const bindAsArray = (vm, key, source, options) => {
if (!vm.state[key]) {
// set value to mobx or simple component
firebaseMutations[MOBXFIRE_VALUE](vm.state, { key, value: [] })
}
const array = vm.state[key]
const cancelCallback = options.cancelCallback
const onAdd = source.on(
"child_added",
(snapshot, prevKey) => {
const index = prevKey ? indexForKey(array, prevKey) + 1 : array.length
const record = createRecord(snapshot, options.model, options)
firebaseMutations[MOBXFIRE_ARRAY_ADD](vm.state, {
key,
index,
record,
array
})
options.onChildAdded && options.onChildAdded(record)
},
cancelCallback
)
const onRemove = source.on(
"child_removed",
snapshot => {
const index = indexForKey(array, getKey(snapshot))
const record = array[index]
firebaseMutations[MOBXFIRE_ARRAY_REMOVE](vm.state, {
key,
index,
record,
array
})
options.onChildRemoved && options.onChildRemoved(record)
},
cancelCallback
)
const onChange = source.on(
"child_changed",
snapshot => {
const index = indexForKey(array, getKey(snapshot))
const record = createRecord(snapshot, options.model, options)
firebaseMutations[MOBXFIRE_ARRAY_CHANGE](vm.state, {
key,
index,
record,
array
})
options.onChildChanged && options.onChildChanged(record)
},
cancelCallback
)
const onMove = source.on(
"child_moved",
(snapshot, prevKey) => {
const index = indexForKey(array, getKey(snapshot))
const record = createRecord(snapshot, options.model, options)
let newIndex = prevKey ? indexForKey(array, prevKey) + 1 : 0
// TODO refactor
newIndex += index < newIndex ? -1 : 0
firebaseMutations[MOBXFIRE_ARRAY_MOVE](vm.state, {
key,
index,
record,
newIndex,
array
})
},
cancelCallback
)
vm.state.$firebaseListeners[key] = {
child_added: onAdd,
child_removed: onRemove,
child_changed: onChange,
child_moved: onMove
}
}
const bindAsDocument = (vm, key, source, options = {}) => {
const cb = snapshot => {
const value = createDocument(snapshot, options.model, options)
// set value
firebaseMutations[MOBXFIRE_VALUE](vm.state, { key, value })
// on change callback
options.onChange && options.onChange(value)
}
const { cancelCallback = () => {} } = options
if (options.once) {
return source
.get()
.then(cb)
.catch(cancelCallback)
}
const unsubscribe = source.onSnapshot(cb, cancelCallback)
vm.state.$firebaseUnsubscribe[key] = unsubscribe
}
const bindAsCollection = (vm, key, source, options) => {
if (!vm.state[key]) {
// set value to vuex or simple component
firebaseMutations[MOBXFIRE_VALUE](vm.state, { key, value: [] })
}
const cb = querySnapshot => {
// set value to vuex or simple component
const value = []
querySnapshot.forEach(function(doc) {
value.push(createDocument(doc, options.model, options))
})
firebaseMutations[MOBXFIRE_VALUE](vm.state, { key, value: value })
}
const { cancelCallback = () => {} } = options
if (options.once) {
return source
.get()
.then(cb)
.catch(cancelCallback)
}
const unsubscribe = source.onSnapshot(cb, cancelCallback)
vm.state.$firebaseUnsubscribe[key] = unsubscribe
}
// mutations
const firebaseMutations = {
[MOBXFIRE_VALUE](state, { key, value }) {
state[key] = value
},
[MOBXFIRE_ARRAY_ADD](state, { key, index, record, array }) {
array = array || state[key]
array.splice(index, 0, record)
},
[MOBXFIRE_ARRAY_CHANGE](state, { key, index, record, array }) {
array = array || state[key]
array.splice(index, 1, record)
},
[MOBXFIRE_ARRAY_MOVE](state, { key, index, record, newIndex, array }) {
array = array || state[key]
array.splice(newIndex, 0, array.splice(index, 1)[0])
},
[MOBXFIRE_ARRAY_REMOVE](state, { key, index, array }) {
array = array || state[key]
array.splice(index, 1)
}
}
// actions
export const firebaseActions = {
bindFirebaseRef({ key, source, options = {} }) {
if (this.$firebaseRefs && this.$firebaseRefs[key]) return
bind({ state: this }, key, source, options)
},
unbindFirebaseRef({ key }) {
unbind({ state: this }, key)
},
bindFirebaseStoreRef({ key, source, options = {} }) {
if (this.$firebaseRefs && this.$firebaseRefs[key]) return
return bindStore({ state: this }, key, source, options)
},
unbindFirebaseStoreRef({ key }) {
return unbind({ state: this }, key)
},
unbindAll() {
if (!this.$firebaseRefs) return
Object.keys(this.$firebaseRefs).forEach(key => {
if (this.$firebaseRefs[key]) {
this.$unbind(key)
}
})
this.$firebaseRefs = null
this.$firebaseSources = null
this.$firebaseListeners = null
this.$firebaseUnsubscribe = null
}
}
import FireModel from "./FireModel"
export default class TodoModel extends FireModel {}
import { observable, computed } from "mobx"
import TodoModel from "./todo-model"
import FireModel from "./fire-model"
export default class TodoListModel extends FireModel {
@observable
todos = []
@computed
get unfinishedTodoCount() {
return this.todos.filter(todo => !todo.finished).length
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment