diff --git a/sample/src/app/app.module.ts b/sample/src/app/app.module.ts
index 7dceb39d5..eb921fede 100644
--- a/sample/src/app/app.module.ts
+++ b/sample/src/app/app.module.ts
@@ -37,6 +37,7 @@ import { MessagingComponent } from './messaging/messaging.component';
import { FunctionsComponent } from './functions/functions.component';
import { FirestoreOfflineComponent } from './firestore-offline/firestore-offline.component';
import { FirestoreOfflineModule } from './firestore-offline/firestore-offline.module';
+import { UpboatsComponent } from './upboats/upboats.component';
@NgModule({
declarations: [
@@ -50,6 +51,7 @@ import { FirestoreOfflineModule } from './firestore-offline/firestore-offline.mo
AuthComponent,
MessagingComponent,
FunctionsComponent,
+ UpboatsComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
diff --git a/sample/src/app/home/home.component.ts b/sample/src/app/home/home.component.ts
index 231414fbf..3f580e958 100644
--- a/sample/src/app/home/home.component.ts
+++ b/sample/src/app/home/home.component.ts
@@ -9,6 +9,7 @@ import { FirebaseApp } from '@angular/fire';
+
diff --git a/sample/src/app/protected-lazy/protected-lazy.component.html b/sample/src/app/protected-lazy/protected-lazy.component.html
index ca6bdab0f..7c96e988f 100644
--- a/sample/src/app/protected-lazy/protected-lazy.component.html
+++ b/sample/src/app/protected-lazy/protected-lazy.component.html
@@ -1 +1,9 @@
protected-lazy works!
+
+
diff --git a/sample/src/app/protected-lazy/protected-lazy.component.ts b/sample/src/app/protected-lazy/protected-lazy.component.ts
index 45525dafa..f84ce2d59 100644
--- a/sample/src/app/protected-lazy/protected-lazy.component.ts
+++ b/sample/src/app/protected-lazy/protected-lazy.component.ts
@@ -1,4 +1,7 @@
import { Component, OnInit } from '@angular/core';
+import { DocumentChangeAction } from '@angular/fire/firestore';
+import { Observable } from 'rxjs';
+import { AngularFirestoreOffline } from '../firestore-offline/firestore-offline.module';
@Component({
selector: 'app-protected-lazy',
@@ -7,7 +10,11 @@ import { Component, OnInit } from '@angular/core';
})
export class ProtectedLazyComponent implements OnInit {
- constructor() { }
+ public snapshot: Observable[]>;
+
+ constructor(private afs: AngularFirestoreOffline) {
+ this.snapshot = afs.collection('test').snapshotChanges();
+ }
ngOnInit(): void {
}
diff --git a/sample/src/app/upboats/upboats.component.css b/sample/src/app/upboats/upboats.component.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/sample/src/app/upboats/upboats.component.html b/sample/src/app/upboats/upboats.component.html
new file mode 100644
index 000000000..7081505e1
--- /dev/null
+++ b/sample/src/app/upboats/upboats.component.html
@@ -0,0 +1,11 @@
+
+ -
+ {{ animal.name }}
+
+ {{ animal.upboats }}
+
+ 🕒
+
+
+
+
diff --git a/sample/src/app/upboats/upboats.component.spec.ts b/sample/src/app/upboats/upboats.component.spec.ts
new file mode 100644
index 000000000..d18bb2c99
--- /dev/null
+++ b/sample/src/app/upboats/upboats.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UpboatsComponent } from './upboats.component';
+
+describe('UpboatsComponent', () => {
+ let component: UpboatsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ UpboatsComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UpboatsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/sample/src/app/upboats/upboats.component.ts b/sample/src/app/upboats/upboats.component.ts
new file mode 100644
index 000000000..4fa023171
--- /dev/null
+++ b/sample/src/app/upboats/upboats.component.ts
@@ -0,0 +1,65 @@
+import { Component, OnInit } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map, startWith, tap } from 'rxjs/operators';
+import { AngularFirestoreOffline } from '../firestore-offline/firestore-offline.module';
+import firebase from 'firebase/app';
+import { makeStateKey, TransferState } from '@angular/platform-browser';
+import { trace } from '@angular/fire/performance';
+
+type Animal = { name: string, upboats: number, id: string, hasPendingWrites: boolean };
+
+@Component({
+ selector: 'app-upboats',
+ templateUrl: './upboats.component.html',
+ styleUrls: ['./upboats.component.css']
+})
+export class UpboatsComponent implements OnInit {
+
+ public animals: Observable;
+
+ constructor(private firestore: AngularFirestoreOffline, state: TransferState) {
+ const collection = firestore.collection('animals', ref =>
+ ref.orderBy('upboats', 'desc').orderBy('updatedAt', 'desc')
+ );
+ const key = makeStateKey(collection.ref.path);
+ const existing = state.get(key, undefined);
+ this.animals = collection.snapshotChanges().pipe(
+ trace('animals'),
+ map(it => it.map(change => ({
+ ...change.payload.doc.data(),
+ id: change.payload.doc.id,
+ hasPendingWrites: change.payload.doc.metadata.hasPendingWrites
+ }))),
+ existing ? startWith(existing) : tap(it => state.set(key, it))
+ );
+ }
+
+ ngOnInit(): void {
+ }
+
+ upboat(id: string) {
+ // TODO add rule
+ this.firestore.doc(`animals/${id}`).update({
+ upboats: firebase.firestore.FieldValue.increment(1),
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ });
+ }
+
+ downboat(id: string) {
+ // TODO add rule
+ this.firestore.doc(`animals/${id}`).update({
+ upboats: firebase.firestore.FieldValue.increment(-1),
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ });
+ }
+
+ newAnimal() {
+ // TODO add rule
+ this.firestore.collection('animals').add({
+ name: prompt('Can haz name?'),
+ upboats: 1,
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ });
+ }
+
+}
diff --git a/src/firestore/collection/changes.ts b/src/firestore/collection/changes.ts
index 07f0d12ba..080bff5d0 100644
--- a/src/firestore/collection/changes.ts
+++ b/src/firestore/collection/changes.ts
@@ -1,6 +1,6 @@
import { fromCollectionRef } from '../observable/fromRef';
import { Observable, SchedulerLike } from 'rxjs';
-import { map, scan } from 'rxjs/operators';
+import { distinctUntilChanged, map, pairwise, scan, startWith } from 'rxjs/operators';
import { DocumentChange, DocumentChangeAction, DocumentChangeType, Query } from '../interfaces';
@@ -25,8 +25,29 @@ export function sortedChanges(
scheduler?: SchedulerLike): Observable[]> {
return fromCollectionRef(query, scheduler)
.pipe(
- map(changes => changes.payload.docChanges()),
- scan((current, changes) => combineChanges(current, changes, events), []),
+ startWith(undefined),
+ pairwise(),
+ scan((current, [priorChanges, changes]) => {
+ const docChanges = changes.payload.docChanges();
+ const ret = combineChanges(current, docChanges, events);
+ // docChanges({ includeMetadataChanges: true }) does't include metadata changes... wat?
+ if (events.indexOf('modified') > -1 && priorChanges &&
+ JSON.stringify(priorChanges.payload.metadata) !== JSON.stringify(changes.payload.metadata)) {
+ return ret.map(it => {
+ const partOfDocChanges = !!docChanges.find(d => d.doc.ref.isEqual(it.doc.ref));
+ return {
+ // if it's not one of the changed docs that means we already saw it's order change
+ // so this is purely metadata, so don't move the doc
+ oldIndex: partOfDocChanges ? it.oldIndex : it.newIndex,
+ newIndex: it.newIndex,
+ type: 'modified',
+ doc: changes.payload.docs.find(d => d.ref.isEqual(it.doc.ref))
+ };
+ });
+ }
+ return ret;
+ }, []),
+ distinctUntilChanged(), // cut down on unneed change cycles
map(changes => changes.map(c => ({ type: c.type, payload: c } as DocumentChangeAction))));
}
@@ -44,6 +65,21 @@ export function combineChanges(current: DocumentChange[], changes: Documen
return current;
}
+/**
+ * Splice arguments on top of a sliced array, to break top-level ===
+ * this is useful for change-detection
+ */
+function sliceAndSplice(
+ original: T[],
+ start: number,
+ deleteCount: number,
+ ...args: T[]
+): T[] {
+ const returnArray = original.slice();
+ returnArray.splice(start, deleteCount, ...args);
+ return returnArray;
+}
+
/**
* Creates a new sorted array from a new change.
*/
@@ -53,7 +89,7 @@ export function combineChange(combined: DocumentChange[], change: Document
if (combined[change.newIndex] && combined[change.newIndex].doc.ref.isEqual(change.doc.ref)) {
// Not sure why the duplicates are getting fired
} else {
- combined.splice(change.newIndex, 0, change);
+ return sliceAndSplice(combined, change.newIndex, 0, change);
}
break;
case 'modified':
@@ -61,16 +97,18 @@ export function combineChange(combined: DocumentChange[], change: Document
// When an item changes position we first remove it
// and then add it's new position
if (change.oldIndex !== change.newIndex) {
- combined.splice(change.oldIndex, 1);
- combined.splice(change.newIndex, 0, change);
+ const copiedArray = combined.slice();
+ copiedArray.splice(change.oldIndex, 1);
+ copiedArray.splice(change.newIndex, 0, change);
+ return copiedArray;
} else {
- combined.splice(change.newIndex, 1, change);
+ return sliceAndSplice(combined, change.newIndex, 1, change);
}
}
break;
case 'removed':
if (combined[change.oldIndex] && combined[change.oldIndex].doc.ref.isEqual(change.doc.ref)) {
- combined.splice(change.oldIndex, 1);
+ return sliceAndSplice(combined, change.oldIndex, 1);
}
break;
}