0

I've build an Angular 8 quiz application with an ASP.NET Core 3 backend. Bear with me as I am pretty new to Angular. The app registers users, logs them in, allows them to create quizzes and add and edit questions related to quizzes. Right now, when they take a quiz and click the 'Submit' button, a dialog pops up with their correct Answers / total questions, see below. The code for that dialog is below as well.

dialog screenshot

playquiz.component.html (left out some code for brevity, notice the Finish Quiz button)

 <mat-list *ngFor="let question of pagedList">
          <span>Question {{pageIndex + 1}}:</span><mat-list-item class="clickLink" [innerHTML]="question.questionText"></mat-list-item>
          <mat-radio-group [(ngModel)]="question.selectedAnswer">    
            <mat-radio-button appearance="outline" class="fullWidth" *ngFor="let answer of question.answers" [value]="answer">
                <span [innerHTML]="answer"></span>
            </mat-radio-button>
            </mat-radio-group><br /><br />
        <button *ngIf="pageIndex == length - 1" mat-button color="primary" (click)="finish()">Finish Quiz</button>
<button *ngIf="pageIndex == length - 1" mat-button color="primary" [routerLink]="['/quizzes', quiz.quizId]">Post Quiz</button>
        </mat-list>

playquiz.component.ts

import { Component } from '@angular/core'
import { ApiService } from './api.service'
import { ActivatedRoute } from '@angular/router'
import {MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import { FinishedComponent } from './finished.component'
import {PageEvent} from '@angular/material';


@Component({
  templateUrl: './playQuiz.component.html'
})
export class PlayQuizComponent {

    constructor(private api: ApiService, private route: ActivatedRoute, private dialog: MatDialog) {}

    quizId
    questions
    quizAttempt = {}

    pagedList = []
    length = 0
    pageSize = 1
    pageIndex = 0
    
 
  ngOnInit(){
    this.quizId = this.route.snapshot.paramMap.get('quizId')
    this.api.getQuestions(this.quizId).subscribe(res => {
      this.questions = res
      
        //first, create Answers list
        this.questions.forEach(q => {
            q.answers = [ q.correctAnswer, q.answer1, q.answer2, q.answer3 ]
            
            // Then, shuffle the answers array
            shuffle(q.answers)
        });
        this.pagedList = this.questions.slice(0, this.pageSize); 
        this.length = this.questions.length; 
    })
  }

  // for Submit button, tally the correct answers and score the quiz
  finish(){
      var correct = 0;
      this.questions.forEach(q => {
          if (q.correctAnswer == q.selectedAnswer)
            correct++
      });
      const dialogRef = this.dialog.open(FinishedComponent, {
        data: { correct, total: this.questions.length}
      });
      console.log(correct)
  }
  OnPageChange(event: PageEvent){
    let startIndex = event.pageIndex * event.pageSize;
    let endIndex = startIndex + event.pageSize;

    if(endIndex > this.length){
      endIndex = this.length;
    }
    this.pagedList = this.questions.slice(startIndex, endIndex);
    this.pageIndex = event.pageIndex
  }
 post(quizAttempt){
    quizAttempt.quizId = this.quizId
    quizAttempt.correctAnswers = 5 // hard-coded for testing
    quizAttempt.attemptDate = Date.now()
    this.api.postQuizAttempt(quizAttempt)
  }

}
// Used ES6 version - https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
function shuffle(a){
    for (let i = a.length; i; i--){
        let j = Math.floor(Math.random() * i);
        [a[i - 1], a[j]] = [a[j], a[i - 1]];
    }
}

api.service.ts

postQuizAttempt(quizAttempt){
        this.http.post(`http://localhost:21031/api/quizzes/${quizAttempt.quizId}`, quizAttempt).subscribe(res => {
            console.log(res)
        })
    }

app.module.ts (routes)

const routes = [
  { path: '', component: HomeComponent },
  { path: 'question', component: QuestionComponent },
  { path: 'question/:quizId', component: QuestionComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'login', component: LoginComponent },
  { path: 'quiz', component: QuizComponent },
  { path: 'play', component: PlayComponent },
  { path: 'playQuiz/:quizId', component: PlayQuizComponent },
  { path: 'quizzes/:quizId', component: PlayQuizComponent }
]

In addition to displaying the user's quiz score, I want to store their "attempt" in the database. I created a model in ASP.NET Core backend called QuizAttempt.cs with the properties I need to store, and in turn I used EF Core to create a table called QuizAttempts. I created an endpoint in my QuizzesController.cs. That code is below. I've tested the backend endpoint in Postman and it is working and storing the data correctly in the database.

I've tried passing the URL parameter in Angular (see above in the app.module.ts and the second button on playQuiz.component.html), like I'm doing with the questions component, but I keep getting 'cannot get property quizId from undefined' even though the quizId parameter is clearly in the URL and it is in the console.log. I'm not sure what to add to my finish() function to get it to store the quizAttempt in the database. I've added the endpoint to my api.service.ts in Angular, see above. I don't understand what I'm missing. I'm stuck, please help! (Please go easy on me, as I am a newbie in Angular and trying to learn.)

QuizzesController.cs

[Authorize]
[HttpPost("{id}")]
public async Task<ActionResult<QuizAttempt>> PostQuizAttempt(int id, QuizAttempt quizAttempt)
{
            var userId = HttpContext.User.Claims.First().Value;

            quizAttempt.UserId = userId;

            var quiz = await _context.Quiz.FindAsync(id);
            quizAttempt.QuizId = quiz.QuizId;

            _context.QuizAttempts.Add(quizAttempt);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetQuizAttempt", new { id = quizAttempt.Id }, quizAttempt);

}

QuizAttempt.cs

public class QuizAttempt
    {
        public int Id { get; set; }
        public string UserId { get; set; }
        public int QuizId { get; set; }
        public DateTime AttemptDate { get; set; }
        public int CorrectAnswers { get; set; }

    }
Katherine
  • 3,318
  • 1
  • 13
  • 17
  • 1
    The runtime binder converts json into server-side models and it's by default case sensitive. This means the Angular model and server models must match case. – JWP Oct 19 '20 at 20:48

1 Answers1

0

Ok, so I figured out what I was missing. There may be better, more elegant ways of solving this but this worked for me. If anyone has any better suggestions, please feel free to post. I had to initialize a quizAttempt object with some default property values on my playQuiz.component.ts like so:

quizAttempt = {
      quizId: 0,
      attemptDate: new Date().toDateString(),
      correctAnswers: 0
    }

Then, I added the following to my finish() function on the playQuiz component:

finish(){
      var correct = 0;
      this.questions.forEach(q => {
          if (q.correctAnswer == q.selectedAnswer)
            correct++
      });
      const dialogRef = this.dialog.open(FinishedComponent, {
        data: { correct, total: this.questions.length}
      });

      //store quiz score
      this.quizAttempt.quizId = this.quizId
      this.quizAttempt.attemptDate = new Date().toDateString()
      this.quizAttempt.correctAnswers = correct

      this.api.postQuizAttempt(this.quizAttempt)
      console.log(this.quizAttempt)
  }

And it worked like a charm! When user clicks "Submit Quiz" it displays a dialog box with their results (score) and behind the scenes, it saves their score and info in the database. Suh-weet!

Katherine
  • 3,318
  • 1
  • 13
  • 17