[go: nahoru, domu]

Skip to content

Commit

Permalink
refactor: tidy up service Promises
Browse files Browse the repository at this point in the history
  • Loading branch information
momargoh committed Feb 5, 2023
1 parent 738de04 commit 138b8ee
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 208 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ I didn't get around to doing this, but I should have put icons on all the button

## BaseComponent

I like to have a BaseComponent (saved in `src/app/shared/components/base.component.ts`) which has a simple `addSubscriptions` method that takes a variable list of `Subscription` and handles correctly unsubscribing from them in the `ngOnDestroy` method.
I like to have a BaseComponent (saved in `src/app/shared/components/base.component.ts`) which has a simple `addSubscriptions` method that takes a variable list of `Subscription` and handles correctly unsubscribing from them in the `ngOnDestroy` method. I put this in initially but haven't really needed to use this in this project, as I've been able to leave the handling to the async pipe in the HTML.

## DataModels

Expand Down
93 changes: 33 additions & 60 deletions src/app/journal/data/services/entry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,7 @@ import {
docData,
updateDoc,
} from '@angular/fire/firestore';
import {
Observable,
combineLatest,
from,
map,
of,
switchMap,
take,
} from 'rxjs';
import { Observable, firstValueFrom, map } from 'rxjs';
import { Entry, EntrySerialized } from '../models/entry';
export type CreateEntryParams = { title: string; content: string };

Expand Down Expand Up @@ -48,6 +40,13 @@ export class EntryService {
);
}

getEntry(id: string): Observable<Entry> {
const entryRef = doc(this.firestore, `entries/${id}`);
return docData(entryRef).pipe(
map((json) => Entry.deserialize(json as EntrySerialized))
);
}

createEntry(
params: CreateEntryParams
): Promise<DocumentReference<DocumentData>> {
Expand All @@ -56,63 +55,37 @@ export class EntryService {
return addDoc(entriesRef, payload);
}

updateEntry(id: string, params: CreateEntryParams) {
async updateEntry(id: string, params: CreateEntryParams): Promise<any> {
// the logic here is to copy the current state of the Entry to the `edits` subcollection
// then update the original Entry, so that the Entry is always the most recent version
return this.getEntry(id).pipe(
take(1),
switchMap((entry) => {
const editsRef = collection(this.firestore, `entries/${id}/edits`);
return from(
addDoc(editsRef, {
timestamp: Timestamp.fromDate(entry.timestamp),
content: entry.content,
title: entry.title,
})
);
}),
switchMap((res) => {
const entriesRef = doc(this.firestore, `entries/${id}`);
return from(
updateDoc(entriesRef, { ...params, timestamp: Timestamp.now() })
);
})
);
const originalEntry = await firstValueFrom(this.getEntry(id));
await addDoc(collection(this.firestore, `entries/${id}/edits`), {
timestamp: Timestamp.fromDate(originalEntry.timestamp),
content: originalEntry.content,
title: originalEntry.title,
});
return updateDoc(doc(this.firestore, `entries/${id}`), {
...params,
timestamp: Timestamp.now(),
});
}

getEntry(id: string): Observable<Entry> {
const entryRef = doc(this.firestore, `entries/${id}`);
return docData(entryRef).pipe(
map((json) => Entry.deserialize(json as EntrySerialized))
);
}

deleteEntry(id: string) {
async deleteEntry(id: string): Promise<any> {
// delete all edits first, wait until this is done and then delete the original Entry document
const editsRef = collection(this.firestore, `entries/${id}/edits`);
return collectionData(editsRef, { idField: 'id' }).pipe(
switchMap((res) => {
if (res.length === 0) {
return of(true);
}
// combineLatest won't emit until all docs in the `edits` subcollection have been deleted
return combineLatest(
res.map((r) => {
return from(
deleteDoc(
doc(
this.firestore,
`entries/${id}/edits/${(r as unknown as { id: string }).id}`
)
)
);
})
);
}),
map((_) => {
// now delete original Entry
return from(deleteDoc(doc(this.firestore, `entries/${id}`)));
const edits = await firstValueFrom(
collectionData(collection(this.firestore, `entries/${id}/edits`), {
idField: 'id',
})
);

edits.forEach(async (edit) => {
await deleteDoc(
doc(
this.firestore,
`entries/${id}/edits/${(edit as unknown as { id: string }).id}`
)
);
});
return deleteDoc(doc(this.firestore, `entries/${id}`));
}
}
6 changes: 2 additions & 4 deletions src/app/journal/journal.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,17 @@ import { WriteEntryPage } from './write-entry/write-entry.page';
styleUrls: ['./journal.page.scss'],
})
export class JournalPage extends Base implements OnInit {
constructor(private modalCtrl: ModalController) {
constructor(private modalController: ModalController) {
super();
}

ngOnInit() {}

async createEntry() {
const modal = await this.modalCtrl.create({
const modal = await this.modalController.create({
component: WriteEntryPage,
backdropDismiss: false,
});
modal.present();

const { data, role } = await modal.onWillDismiss();
}
}
33 changes: 12 additions & 21 deletions src/app/journal/list-entries/list-entries.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Base } from 'src/app/shared/components/base.component';
import { EntryService } from '../data/services/entry.service';
import { Observable, map, tap } from 'rxjs';
import { ViewEntryPage } from '../view-entry/view-entry.page';
import { LoadingController, ModalController } from '@ionic/angular';
import { ModalController } from '@ionic/angular';
import { LoadingService } from 'src/app/services/loading.service';

@Component({
Expand All @@ -17,33 +17,26 @@ import { LoadingService } from 'src/app/services/loading.service';
})
export class ListEntriesComponent extends Base implements OnInit {
entries$: Observable<Entry[]>;
loading: HTMLIonLoadingElement;

constructor(
private entryService: EntryService,
private modalController: ModalController,
private loadingController: LoadingController,
private loadingService: LoadingService
) {
super();
}

ngOnInit() {
this.addSubscriptions(
this.loadingService.create('Loading the journal...').subscribe({
next: () => {
// sorting of the entries is performed here and not in the service to permit different components to order the entries how they want
this.entries$ = this.entryService.listEntries().pipe(
map((entries) => {
return entries.sort((a, b) => {
return b.timestamp.valueOf() - a.timestamp.valueOf();
});
}),
tap(() => {
this.loadingService.dismiss();
})
);
},
async ngOnInit() {
await this.loadingService.create('Loading the journal...');
// sorting of the entries is performed here and not in the service to permit different components to order the entries how they want
this.entries$ = this.entryService.listEntries().pipe(
map((entries) => {
return entries.sort((a, b) => {
return b.timestamp.valueOf() - a.timestamp.valueOf();
});
}),
tap(() => {
this.loadingService.dismiss();
})
);
}
Expand All @@ -55,7 +48,5 @@ export class ListEntriesComponent extends Base implements OnInit {
componentProps: { id: entry.id },
});
modal.present();

const { data, role } = await modal.onWillDismiss();
}
}
136 changes: 67 additions & 69 deletions src/app/journal/view-entry/view-entry.page.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { QuillModule } from 'ngx-quill';
import {
ModalController,
AlertController,
ToastController,
LoadingController,
} from '@ionic/angular';
import { ModalController, ToastController } from '@ionic/angular';
import { SharedModule } from 'src/app/shared/shared.module';
import { EntryService } from '../data/services/entry.service';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
Expand All @@ -32,6 +27,7 @@ import { WriteEntryPage } from '../write-entry/write-entry.page';
})
export class ViewEntryPage extends Base implements OnInit {
@Input() id: string;

sanitizedContent: SafeHtml;
entry$: Observable<Entry>;
edits$: Observable<
Expand All @@ -42,94 +38,96 @@ export class ViewEntryPage extends Base implements OnInit {
sanitizedContent: SafeHtml;
}[]
>;

// observable to cut off the stream of entry$ and edit$
private deleteCalledSource = new Subject<void>();
private deleteCalled$ = this.deleteCalledSource.asObservable();

constructor(
private entryService: EntryService,
private modalController: ModalController,
private alertController: AlertController,
private toastController: ToastController,
private loadingService: LoadingService,
private domSanitizer: DomSanitizer
) {
super();
}

ngOnInit() {
this.loadingService.create('Loading entry...').subscribe({
next: () => {
this.entry$ = this.entryService.getEntry(this.id).pipe(
tap((entry) => {
// sanitize the Entry content HTML
this.sanitizedContent = this.domSanitizer.bypassSecurityTrustHtml(
entry.content
);
}),
takeUntil(this.deleteCalled$) // unsubscribes if delete is called
);
this.edits$ = this.entryService.listEdits(this.id).pipe(
map((edits) => {
return (
edits
// order by timestamp
.sort((a, b) => {
return b.timestamp.valueOf() - a.timestamp.valueOf();
})
// sanitize the HTML
.map((edit) => {
return {
...edit,
sanitizedContent: this.domSanitizer.bypassSecurityTrustHtml(
edit.content
),
};
})
);
}),
takeUntil(this.deleteCalled$)
async ngOnInit() {
await this.loadingService.create('Loading entry...');

this.entry$ = this.entryService.getEntry(this.id).pipe(
tap((entry) => {
// sanitize the Entry content HTML
this.sanitizedContent = this.domSanitizer.bypassSecurityTrustHtml(
entry.content
);
// wait until entry$ and edit$ have emitted once before dismissing the loading spinner
this.addSubscriptions(
combineLatest([this.entry$, this.edits$])
.pipe(take(1))
.subscribe({
next: () => {
this.loadingService.dismiss();
},
}),
takeUntil(this.deleteCalled$) // unsubscribes if delete is called
);
this.edits$ = this.entryService.listEdits(this.id).pipe(
map((edits) => {
return (
edits
// order by timestamp
.sort((a, b) => {
return b.timestamp.valueOf() - a.timestamp.valueOf();
})
// sanitize the HTML
.map((edit) => {
return {
...edit,
sanitizedContent: this.domSanitizer.bypassSecurityTrustHtml(
edit.content
),
};
})
);
},
});
}),
takeUntil(this.deleteCalled$)
);
// wait until entry$ and edit$ have emitted once before dismissing the loading spinner
combineLatest([this.entry$, this.edits$])
.pipe(take(1))
.subscribe({
next: () => {
this.loadingService.dismiss();
},
});
}

close() {
this.modalController.dismiss(null, 'close');
}

update() {
this.modalController
.create({
component: WriteEntryPage,
componentProps: { mode: 'update', updateId: this.id },
})
.then((m) => m.present());
async update() {
const modal = await this.modalController.create({
component: WriteEntryPage,
componentProps: { mode: 'update', updateId: this.id },
});
modal.present();
}

delete() {
async delete() {
// unsubscribe from entry$ and edit$ to prevent errors from async subscriptions in html file
this.deleteCalledSource.next();
this.entryService.deleteEntry(this.id).subscribe({
next: () => {
this.modalController.dismiss(null, 'close');
this.toastController
.create({
message: 'Successfully deleted Entry.',
duration: 1500,
position: 'top',
})
.then((toast) => toast.present());
},
});
try {
await this.entryService.deleteEntry(this.id);
this.modalController.dismiss(null, 'close');
const toast = await this.toastController.create({
message: 'Successfully deleted Entry.',
duration: 1500,
position: 'top',
});
toast.present();
} catch (error) {
this.modalController.dismiss(null, 'fail');
const toast = await this.toastController.create({
message: 'Failed to delete Entry.',
duration: 1500,
position: 'top',
});
toast.present();
}
}
}
Loading

0 comments on commit 138b8ee

Please sign in to comment.