Skip to content
This repository was archived by the owner on May 19, 2025. It is now read-only.
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ed9c8b5
Increase efficiency of Array updates made by FirestoreMixin.
merlinnot Oct 23, 2017
6d5a55a
Merge branch 'firestore' into fix-issue-283
merlinnot Oct 25, 2017
d0a4870
Fix an issue with initail binding in FirestoreMixin.
merlinnot Oct 25, 2017
e284cc4
FirestoreMixin: throw an error on binding to properties with invalid …
merlinnot Oct 25, 2017
28210c0
FirestoreMixin: ensure that it was not previously applied to the same…
merlinnot Oct 25, 2017
3644a85
FirestoreMixin: add missing semicolons.
merlinnot Oct 25, 2017
5982940
Add query property to _firestoreProps in FirestoreMixin.
merlinnot Oct 26, 2017
b0b30b5
Merge branch 'firestore-mixin-fixes' into fix-issue-302
merlinnot Nov 2, 2017
18ac794
Assign a null value to a document when it's listener is being deleted.
merlinnot Jan 3, 2018
ef9c2d0
Strict & more readable comparison in _firestoreAssignCollection metho…
merlinnot Jan 3, 2018
90f4d92
Merge branch 'fix-issue-283' into firestore
merlinnot Jan 3, 2018
bc292aa
Merge branch 'firestore' into fix-issue-284
merlinnot Jan 3, 2018
35caf3a
Apply DRY rule and improve a name of _ensurePropertyTypeCorrectness i…
merlinnot Jan 3, 2018
1a60e76
Merge branch 'firestore' into fix-issue-292
merlinnot Jan 3, 2018
5e5245e
Handle applying FirestoreMixin multiple times to the same element.
merlinnot Jan 3, 2018
fd12a0d
Simplify syntax in TRANSFORMS in FirestoreMixin.
merlinnot Jan 3, 2018
e148a13
Merge branch 'firestore' into fix-issue-300
merlinnot Jan 3, 2018
97dac28
Merge branch 'firestore' into fix-issue-302
merlinnot Jan 3, 2018
28f3e7d
Fix an issue with initial binding of properties in FirestoreMixin.
merlinnot Jan 4, 2018
cbd7370
Fix 'propertyReady' in FirestoreMixin.
merlinnot Jan 8, 2018
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
193 changes: 141 additions & 52 deletions firebase-firestore-mixin.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
}

{
const CONSTRUCTOR_TOKEN = Symbol('polymerfire-firestore-mixin-constructor');
const CONNECTED_CALLBACK_TOKEN =
Symbol('polymerfire-firestore-mixin-connected-callback');
const PROPERTY_BINDING_REGEXP = /{([^{]+)}/g;
const TRANSFORMS = {
doc: function(snap) { return iDoc(snap); },
collection: function(snap) { return snap.empty ? [] : snap.docs.map(doc => iDoc(doc)) }
}

const isOdd = (x) => x & 1 === 1;

Expand All @@ -31,14 +30,14 @@
return whole;
}

const collect = (what, which) => {
let res = {};
while (what) {
res = Object.assign({}, what[which], res); // Respect prototype priority
what = Object.getPrototypeOf(what);
}
return res;
};
const collect = (what, which) => {
let res = {};
while (what) {
res = Object.assign({}, what[which], res); // Respect prototype priority
what = Object.getPrototypeOf(what);
}
return res;
};

const iDoc = (snap) => {
if (snap.exists) {
Expand All @@ -48,6 +47,11 @@
}
}

const TRANSFORMS = {
doc: iDoc,
collection: (snap) => snap.empty ? [] : snap.docs.map(iDoc),
}

/**
* This mixin provides bindings to documents and collections in a
* Cloud Firestore database through special property declarations.
Expand Down Expand Up @@ -131,82 +135,167 @@
*/
Polymer.FirestoreMixin = parent => {
return class extends parent {
static _assertPropertyTypeCorrectness(prop) {
const errorMessage = (listenerType, propertyType) =>
`FirestoreMixin's ${listenerType} can only be used with properties ` +
`of type ${propertyType}.`;
const assert = (listenerType, propertyType) => {
if (prop[listenerType] !== undefined && prop.type !== propertyType) {
throw new Error(errorMessage(listenerType, propertyType.name));
}
}

assert('doc', Object);
assert('collection', Array);
}

constructor() {
super();

if (this[CONSTRUCTOR_TOKEN] === true) {
return;
}
this[CONSTRUCTOR_TOKEN] = true;

this._firestoreProps = {};
this._firestoreListeners = {};
this.db = this.constructor.db || firebase.firestore();
}

connectedCallback() {
if (this[CONNECTED_CALLBACK_TOKEN] === true) {
return;
}
this[CONNECTED_CALLBACK_TOKEN] = true;

const props = collect(this.constructor, 'properties');
Object
.values(props)
.forEach(this.constructor._assertPropertyTypeCorrectness);

for (let name in props) {
if (props[name].doc) {
this._firestoreBind('doc', props[name].doc, name, props[name].live, props[name].observes);
} else if (props[name].collection) {
this._firestoreBind('collection', props[name].collection, name, props[name].live, props[name].observes);
const options = props[name];
if (options.doc || options.collection) {
this._firestoreBind(name, options);
}
}
super.connectedCallback();
}

_firestoreBind(type, path, name, live = false, observes = []) {
const config = parsePath(path);
config.observes = observes;
config.live = live;
_firestoreBind(name, options) {
const defaults = {
live: false,
observes: [],
}
const parsedPath = parsePath(options.doc || options.collection);
const config = Object.assign({}, defaults, options, parsedPath);
const type = config.type =
config.doc ? 'doc' : config.collection ? 'collection' : undefined;

this._firestoreProps[name] = config;

// Create a method observer that will be called every time a templatized or observed property changes
let args = config.props.concat(config.observes).join(',');
if (args.length) { args = ',' + args; }
this._createMethodObserver(`_firestoreUpdateBinding('${type}','${name}'${args})`);

if (!config.props.length && !config.observes.length) {
this._firestoreUpdateBinding(type,name);
const args = config.props.concat(config.observes);
if (args.length > 0) {
// Create a method observer that will be called every time
// a templatized or observed property changes
const observer =
`_firestoreUpdateBinding('${name}', ${args.join(',')})`
this._createMethodObserver(observer);
}

this._firestoreUpdateBinding(name, ...args.map(x => this[x]));
}

_firestoreUpdateBinding(type, name) {
_firestoreUpdateBinding(name, ...args) {
this._firestoreUnlisten(name);

const config = this._firestoreProps[name];
const propArgs = Array.prototype.slice.call(arguments, 2, config.props.length + 2).filter(arg => arg);
const observesArgs = Array.prototype.slice.call(arguments, config.props.length + 2).filter(arg => arg);
const isDefined = (x) => x !== undefined;
const propArgs = args.slice(0, config.props.length).filter(isDefined);
const observesArgs = args.slice(config.props.length).filter(isDefined);

if (propArgs.length < config.props.length || observesArgs.length < config.observes.length) {
this[name] = null;
this[name + 'Ref'] = null;
this[name + 'Ready'] = false;
return;
}
const propArgsReady = propArgs.length === config.props.length;
const observesArgsReady =
observesArgs.length === config.observes.length;

const collPath = stitch(config.literals, propArgs);
const assigner = snap => {
this[name] = TRANSFORMS[type](snap);
this[name + 'Ready'] = true;
}
if (propArgsReady && observesArgsReady) {
const collPath = stitch(config.literals, propArgs);
const assigner = this._firestoreAssigner(name, config.type);

let ref = this.db[type](collPath);
this[name + 'Ref'] = ref;
this[name + 'Ready'] = false;
let ref = this.db[config.type](collPath);
this[name + 'Ref'] = ref;

if (config.query) {
ref = config.query.call(this, ref, this);
}
if (config.query) {
ref = config.query.call(this, ref, this);
}

if (config.live) {
this._firestoreListeners[name] = ref.onSnapshot(assigner);
} else {
ref.get().then(assigner);
if (config.live) {
this._firestoreListeners[name] = ref.onSnapshot(assigner);
} else {
ref.get().then(assigner);
}
}
}

_firestoreUnlisten(name) {
_firestoreUnlisten(name, type) {
if (this._firestoreListeners[name]) {
this._firestoreListeners[name]();
delete this._firestoreListeners[name];
}


this.setProperties({
[name]: type === 'collection' ? [] : null,
[name + 'Ref']: null,
[name + 'Ready']: false,
})
}

_firestoreAssigner(name, type) {
const makeAssigner = (assigner) => (snap) => {
assigner.call(this, name, snap);
this[name + 'Ready'] = true;
}
if (type === 'doc') {
return makeAssigner(this._firestoreAssignDocument);
} else if (type === 'collection') {
return makeAssigner(this._firestoreAssignCollection);
} else {
throw new Error('Unknown listener type.');
}
}

_firestoreAssignDocument(name, snap) {
this[name] = iDoc(snap);
}

_firestoreAssignCollection(name, snap) {
const propertyValueIsArray = Array.isArray(this[name])
const allDocumentsChanged = snap.docs.length === snap.docChanges.length;
if (propertyValueIsArray && allDocumentsChanged === false) {
snap.docChanges.forEach((change) => {
switch (change.type) {
case 'added':
this.splice(name, change.newIndex, 0, iDoc(change.doc));
break;
case 'removed':
this.splice(name, change.oldIndex, 1);
break;
case 'modified':
if (change.oldIndex === change.newIndex) {
this.splice(name, change.oldIndex, 1, iDoc(change.doc));
} else {
this.splice(name, change.oldIndex, 1);
this.splice(name, change.newIndex, 0, iDoc(change.doc));
}
break;
default:
throw new Error(`Unhandled document change: ${change.type}.`);
}
});
} else {
this[name] = snap.docs.map(iDoc);
}
}
}
}
Expand Down