I could use some help refactoring my component and service. I have a lot of nested logic and wasn't sure what I could do to improve this. I'm new to Angular 7 and their piping syntax. I suspect this is what I could use in this example to improve my code? Also, I have an issue where the drawPoll() function is being called before the forEach iteration is done so I manually added a 3 second setTimeout() for the time being. I think that refactoring these subscriptions to not be so nested and also be synchronous might fix this issue.
poll.component.ts
import { Component, OnInit } from '@angular/core';
import * as Chart from 'chart.js';
import { Observable } from 'rxjs';
import { FirebaseService } from '../services/firebase.service';
import { Input, Output, EventEmitter } from '@angular/core';
import { CardModule } from 'primeng/card';
@Component({
selector: 'app-poll',
templateUrl: './poll.component.html',
styleUrls: ['./poll.component.scss']
})
export class PollComponent implements OnInit {
chart:any;
poll:any;
votes:[] = [];
labels:string[] = [];
title:string = "";
isDrawn:boolean = false;
inputChoices:any = [];
@Input()
pollKey: string;
@Output()
editEvent = new EventEmitter<string>();
@Output()
deleteEvent = new EventEmitter<string>();
constructor(private firebaseService: FirebaseService) { }
ngOnInit() {
this.firebaseService.getPoll(this.pollKey).subscribe(pollDoc => {
// ToDo: draw poll choices on create without breaking vote listener
console.log("details?", pollDoc);
// Return if subscription was triggered due to poll deletion
if (!pollDoc.payload.exists) {
return;
}
const pollData:any = pollDoc.payload.data();
this.poll = {
id: pollDoc.payload.id,
helperText: pollData.helperText,
pollType: pollData.pollType,
scoringType: pollData.scoringType,
user: pollData.user
};
this.title = this.poll.pollType == 1 ? "Title 1" : "Title 2"
this.firebaseService.getChoices(this.pollKey).subscribe(choices => {
this.poll.choices = [];
choices.forEach(choice => {
const choiceData:any = choice.payload.doc.data();
const choiceKey:any = choice.payload.doc.id;
this.firebaseService.getVotes(choiceKey).subscribe((votes: any) => {
console.log("does this get hit on a vote removal?", votes.length);
this.poll.choices.push({
id: choiceKey,
text: choiceData.text,
votes: votes.length
});
})
});
setTimeout(() => {
this.drawPoll();
}, 3000);
});
});
}
drawPoll() {
if (this.isDrawn) {
this.chart.data.datasets[0].data = this.poll.choices.map(choice => choice.votes);
this.chart.data.datasets[0].label = this.poll.choices.map(choice => choice.text);
this.chart.update()
}
if (!this.isDrawn) {
this.inputChoices = this.poll.choices;
var canvas = <HTMLCanvasElement> document.getElementById(this.pollKey);
var ctx = canvas.getContext("2d");
this.chart = new Chart(ctx, {
type: 'horizontalBar',
data: {
labels: this.poll.choices.map(choice => choice.text),
datasets: [{
label: this.title,
data: this.poll.choices.map(choice => choice.votes),
fill: false,
backgroundColor: [
"rgba(255, 99, 132, 0.2)",
"rgba(255, 159, 64, 0.2)",
"rgba(255, 205, 86, 0.2)",
"rgba(75, 192, 192, 0.2)",
"rgba(54, 162, 235, 0.2)",
"rgba(153, 102, 255, 0.2)",
"rgba(201, 203, 207, 0.2)"
],
borderColor: [
"rgb(255, 99, 132)",
"rgb(255, 159, 64)",
"rgb(255, 205, 86)",
"rgb(75, 192, 192)",
"rgb(54, 162, 235)",
"rgb(153, 102, 255)",
"rgb(201, 203, 207)"
],
borderWidth: 1
}]
},
options: {
events: ["touchend", "click", "mouseout"],
onClick: function(e) {
console.log("clicked!", e);
},
tooltips: {
enabled: true
},
title: {
display: true,
text: this.title,
fontSize: 14,
fontColor: '#666'
},
legend: {
display: false
},
maintainAspectRatio: true,
responsive: true,
scales: {
xAxes: [{
ticks: {
beginAtZero: true,
precision: 0
}
}]
}
}
});
this.isDrawn = true;
}
}
vote(choiceId) {
if (choiceId) {
const choiceInput:any = document.getElementById(choiceId);
const checked = choiceInput.checked;
if (checked) this.firebaseService.incrementChoice(choiceId);
if (!checked) this.firebaseService.decrementChoice(choiceId);
this.poll.choices.forEach(choice => {
const choiceEl:any = document.getElementById(choice.id);
if (choiceId !== choiceEl.id && checked) choiceEl.disabled = true;
if (!checked) choiceEl.disabled = false;
});
}
}
}
firebase.service.ts
import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
import { map, switchMap, first } from 'rxjs/operators';
import { Observable, from } from 'rxjs';
import * as firebase from 'firebase';
import { AngularFireAuth } from '@angular/fire/auth';
@Injectable({
providedIn: 'root'
})
export class FirebaseService {
// Source: https://github.com/AngularTemplates/angular-firebase-crud/blob/master/src/app/services/firebase.service.ts
constructor(public db: AngularFirestore, private afAuth: AngularFireAuth) { }
getPoll(pollKey) {
return this.db.collection('polls').doc(pollKey).snapshotChanges();
}
getChoices(pollKey) {
return this.db.collection('choices', ref => ref.where('poll', '==', pollKey)).snapshotChanges();
}
incrementChoice(choiceKey) {
const userId = this.afAuth.auth.currentUser.uid;
const choiceDoc:any = this.db.collection('choices').doc(choiceKey);
// Check if user voted already
choiceDoc.ref.get().then(choice => {
let pollKey = choice.data().poll
this.db.collection('votes').snapshotChanges().pipe(first()).subscribe((votes:any) => {
let filteredVote = votes.filter((vote) => {
const searchedPollKey = vote.payload.doc._document.proto.fields.poll.stringValue;
const searchedChoiceKey = vote.payload.doc._document.proto.fields.choice.stringValue;
const searchedUserKey = vote.payload.doc._document.proto.fields.user.stringValue;
return (searchedPollKey == pollKey && searchedChoiceKey == choiceKey && searchedUserKey == userId);
});
if (filteredVote.length) {
// This person aleady voted
return false;
} else {
let votes = choice.data().votes
choiceDoc.update({
votes: ++votes
});
const userDoc:any = this.db.collection('users').doc(userId);
userDoc.ref.get().then(user => {
let points = user.data().points
userDoc.update({
points: ++points
});
});
this.createVote({
choiceKey: choiceKey,
pollKey: pollKey,
userKey: userId
});
}
});
});
}
decrementChoice(choiceKey) {
const choiceDoc:any = this.db.collection('choices').doc(choiceKey);
const userId = this.afAuth.auth.currentUser.uid;
choiceDoc.ref.get().then(choice => {
let pollKey = choice.data().poll
let votes = choice.data().votes
choiceDoc.update({
votes: --votes
});
const userDoc:any = this.db.collection('users').doc(userId);
userDoc.ref.get().then(user => {
let points = user.data().points
userDoc.update({
points: --points
});
});
// Find & delete vote
this.db.collection('votes').snapshotChanges().pipe(first()).subscribe((votes:any) => {
let filteredVote = votes.filter((vote) => {
const searchedPollKey = vote.payload.doc._document.proto.fields.poll.stringValue;
const searchedChoiceKey = vote.payload.doc._document.proto.fields.choice.stringValue;
const searchedUserKey = vote.payload.doc._document.proto.fields.user.stringValue;
return (searchedPollKey == pollKey && searchedChoiceKey == choiceKey && searchedUserKey == userId);
});
this.deleteVote(filteredVote[0].payload.doc.id);
});
});
}
createVote(value) {
this.db.collection('votes').add({
choice: value.choiceKey,
poll: value.pollKey,
user: value.userKey
}).then(vote => {
console.log("Vote created successfully", vote);
}).catch(err => {
console.log("Error creating vote", err);
});
}
deleteVote(voteKey) {
this.db.collection('votes').doc(voteKey).delete().then((vote) => {
console.log("Vote deleted successfully");
}).catch(err => {
console.log("Error deleting vote", err);
});
}
getVotes(choiceKey) {
return this.db.collection('votes', ref => ref.where('choice', '==', choiceKey)).snapshotChanges().pipe(first());
}
}