Я столкнулся с проблемой, которую мне трудно решить.Есть две проблемы, каждая из которых, по-видимому, вызвана условиями гонки.
1.) Функция drawPoll () выполняется до добавления this.poll.choices.Я подтвердил, что это проблема, добавив вручную 3 секунды setTimeout ().Как мне убедиться, что функция drawPoll () выполняется только после того, как итерация choices.ForEach () завершена?
2.) При вызове функции голосования (choiceId) и уменьшении выбора наблюдаемая firebaseService не выбираетдо правильного значения для «голосов», потому что наблюдаемое вступает в силу до того, как удаление голоса завершится.Как я переставляю свой код так, чтобы наблюдаемое ожидало, пока удаление документа голосования завершено?
Я пытался обернуть итерацию choices.forEach в обещание, но мне было трудно заставить это работать.И я не был уверен, где вообще начинать создавать цепочку обещаний для decmentChoice () и getChoices (), поскольку функция getChoices () не всегда полагается на функцию decmentChoice или incrementChoice (), когда она инициализируется.Это зависит только от тех, кто голосует.Прилагается мой компонент и служба Firebase.Любая помощь будет принята с благодарностью!
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 { first } from 'rxjs/operators';
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 = [];
username:string = "";
points:number;
@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
};
if (this.poll.pollType == 1) {
this.title = "Who Do I Start?";
}
if (this.poll.pollType == 2) {
this.title = "Who Do I Drop?";
}
if (this.poll.pollType == 3) {
this.title = "Who Do I Pick Up?";
}
if (this.poll.pollType == 4) {
this.title = "Who Wins This Trade?";
}
// Populate username and user points
this.firebaseService.getUser(pollData.user).subscribe((user:any) => {
const userDetails = user.payload._document.proto;
if (userDetails) {
this.username = userDetails.fields.username.stringValue;
this.points = userDetails.fields.points.integerValue;
}
});
this.firebaseService.getChoices(this.pollKey).pipe(first()).subscribe(choices => {
console.log("get 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).pipe(first()).subscribe((votes: any) => {
this.poll.choices.push({
id: choiceKey,
text: choiceData.text,
votes: votes.length
});
});
this.firebaseService.getVotes(choiceKey).subscribe((votes: any) => {
if (this.isDrawn) {
const selectedChoice = this.poll.choices.find((choice) => {
return choice.id == choiceKey
});
selectedChoice.votes = votes.length;
this.drawPoll();
}
});
});
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) {
console.log("text?", this.poll.choices.map(choice => choice.text));
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, 4, 40, 0.2)",
"rgba(19, 32, 98, 0.2)",
"rgba(255, 4, 40, 0.2)",
"rgba(19, 32, 98, 0.2)",
"rgba(255, 4, 40, 0.2)",
"rgba(19, 32, 98, 0.2)"
],
borderColor: [
"rgb(255, 4, 40)",
"rgb(19, 32, 98)",
"rgb(255, 4, 40)",
"rgb(19, 32, 98)",
"rgb(255, 4, 40)",
"rgb(19, 32, 98)",
],
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;
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;
});
if (checked) this.firebaseService.incrementChoice(choiceId);
if (!checked) this.firebaseService.decrementChoice(choiceId);
}
}
edit() {
this.editEvent.emit(this.poll);
}
delete() {
this.deleteEvent.emit(this.poll);
}
}
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());
}
}
** ОБНОВЛЕНИЕ **
Мне удалось решить проблему № 2, создав отдельную подписку только для обновлений голосов.Код кажется довольно громоздким, но, по крайней мере, сейчас №2 больше не является проблемой.Я все еще сталкиваюсь с той же проблемой, хотя с # 1, где функция drawPoll () выполняется до того, как this.poll.choices завершит итерацию и добавление.Я обновил вопрос, чтобы отразить мой обновленный код.