Last active
January 10, 2019 10:06
Mobx firebase binding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import FireModel from "./FireModel" | |
export default class TodoModel extends FireModel {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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