2
\$\begingroup\$

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());
  }

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.