Take this awesome quiz that will help improve your understanding of Dependency Injection. The timed questionnaire
+ with automatic scoring provides you with a final score at the end. Match wits with your friends! Practice to
+ increase your knowledge. Good luck and have fun with this quiz. Share and enjoy!
+ Question {{ question.questionId }} of {{ totalQuestions }}
+
+
+ Time
+
+ 0:0{{ timeLeft }}
+
+
+
+
+
+
+
{{ question.question }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 && progressValue <= 100) &&
+ question && question.questionId <= totalQuestions"
+ type="success" [striped]="true" [animated]="true" [value]="progressValue">
+ {{ progressValue.toFixed(0) }}%
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/playground/quiz/src/app/containers/question/question.component.scss b/apps/playground/quiz/src/app/containers/question/question.component.scss
new file mode 100644
index 000000000..058c15297
--- /dev/null
+++ b/apps/playground/quiz/src/app/containers/question/question.component.scss
@@ -0,0 +1,136 @@
+$font-stack: Space Mono, monospace;
+$font-weight-max: 900;
+
+@font-face {
+ font-family: "Alarm Clock";
+ src: url("../../../assets/alarm-clock.ttf") format("truetype");
+}
+
+section.scoreboard {
+ margin-top: 10px !important;
+
+ .row {
+ display: inline;
+ }
+ .score {
+ float: left;
+ margin-left: 1rem;
+ }
+ .score .leader {
+ margin-left: 15px;
+ }
+ .badge {
+ float: left;
+ margin: 20px 10px 0 100px;
+ font-family: $font-stack;
+ font-size: 24px;
+ font-weight: $font-weight-max;
+ font-style: italic;
+ }
+ .time-left {
+ float: right;
+ margin-right: 1rem;
+ }
+ .time-left .leader {
+ margin-left: 20px;
+ }
+ .scoreboard {
+ font-family: "Alarm Clock", $font-stack;
+ font-weight: $font-weight-max;
+ font-size: 30px;
+ color: #006400;
+ display: inline-block;
+ margin: -5px 0 0 15px;
+ width: auto;
+ }
+ .leader {
+ display: block;
+ font-weight: $font-weight-max;
+ font-size: 18px;
+ text-transform: uppercase;
+ position: relative;
+ top: -5px;
+ }
+}
+
+section.time-expired {
+ margin: 50px 0 20px 0 !important;
+ display: flex;
+ justify-content: center;
+
+ button.time-expired-btn {
+ font-family: $font-stack;
+ font-weight: $font-weight-max;
+ text-align: center;
+ font-style: italic;
+ color: #006400 !important;
+ width: 26.5rem;
+ border: 1px solid #ff0000;
+ border-radius: 5px;
+ padding: 5px;
+ margin-bottom: 20px;
+ }
+ .timer-expired-icon, .qa-icon {
+ font-weight: $font-weight-max;
+ font-size: 30px !important;
+ color: #9acd32;
+ }
+ span.proceed, span.viewResults {
+ margin-top: -30px;
+ }
+}
+
+#question {
+ font-family: $font-stack;
+ font-weight: 700;
+ font-size: 30px !important;
+ margin: 0 0 10px 0.4rem;
+ float: left;
+ border: 2px solid #007aff;
+ padding: 5px 10px 15px 20px;
+ background-color: #f5f5f5;
+ color: #0f0900;
+ width: 39rem !important;
+ height: auto;
+ vertical-align: middle;
+}
+
+section.paging {
+ width: 40rem;
+
+ mat-card-actions {
+ margin: -10px 0 10px 0;
+
+ .previousQuestionNav {
+ float: left;
+ margin-left: 1.5rem;
+ }
+ .nextQuestionNav {
+ float: right;
+ margin-right: -0.35rem;
+ }
+ .previousQuestionNav:hover, .nextQuestionNav:hover {
+ border: 1px solid #007aff;
+ }
+ }
+}
+
+section.progress-bar {
+ margin: 40px 0 10px 1.5rem;
+ width: 39rem;
+ height: auto;
+
+ ngb-progressbar {
+ border-radius: 10px;
+ }
+
+ .progress-note {
+ color: #ffff00;
+ font-family: $font-stack;
+ font-weight: $font-weight-max;
+ font-style: italic;
+ font-size: 20px;
+ padding-top: 5px;
+ margin-top: 5px;
+ }
+}
diff --git a/apps/playground/quiz/src/app/containers/question/question.component.ts b/apps/playground/quiz/src/app/containers/question/question.component.ts
new file mode 100644
index 000000000..6399281fb
--- /dev/null
+++ b/apps/playground/quiz/src/app/containers/question/question.component.ts
@@ -0,0 +1,344 @@
+import { Component, OnInit, Input, Output } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { FormGroup } from '@angular/forms';
+
+import { QuizQuestion } from '../../model/QuizQuestion';
+
+@Component({
+ selector: 'codelab-question-container',
+ templateUrl: './question.component.html',
+ styleUrls: ['./question.component.scss']
+})
+export class QuestionComponent implements OnInit {
+ @Input() formGroup: FormGroup;
+ @Output() question: QuizQuestion;
+ @Output() totalQuestions: number;
+ @Output() totalSelections = 0;
+ @Output() totalQuestionsAttempted = 0;
+ @Output() correctAnswersCount = 0;
+ @Output() percentage = 0;
+ @Output() completionTime: number;
+
+ questionID = 0;
+ currentQuestion = 0;
+ questionIndex: number;
+ optionIndex: number;
+ correctAnswer: boolean;
+ disabled: boolean;
+ progressValue: number;
+ timeLeft: number;
+ timePerQuestion = 20;
+ interval: any;
+ elapsedTime: number;
+ elapsedTimes = [];
+ blueBorder = '2px solid #007aff';
+
+ @Output() allQuestions: QuizQuestion[] = [
+ {
+ questionId: 1,
+ question: 'What is the objective of dependency injection?',
+ options: [
+ { optionValue: '1', optionText: 'Pass the service to the client.' },
+ { optionValue: '2', optionText: 'Allow the client to find service.' },
+ { optionValue: '3', optionText: 'Allow the client to build service.' },
+ { optionValue: '4', optionText: 'Give the client part service.' }
+ ],
+ answer: '1',
+ explanation: 'a service gets passed to the client during DI',
+ selectedOption: ''
+ },
+ {
+ questionId: 2,
+ question: 'Which of the following benefit from dependency injection?',
+ options: [
+ { optionValue: '1', optionText: 'Programming' },
+ { optionValue: '2', optionText: 'Testability' },
+ { optionValue: '3', optionText: 'Software design' },
+ { optionValue: '4', optionText: 'All of the above.' },
+ ],
+ answer: '4',
+ explanation: 'DI simplifies both programming and testing as well as being a popular design pattern',
+ selectedOption: ''
+ },
+ {
+ questionId: 3,
+ question: 'Which of the following is the first step in setting up dependency injection?',
+ options: [
+ { optionValue: '1', optionText: 'Require in the component.' },
+ { optionValue: '2', optionText: 'Provide in the module.' },
+ { optionValue: '3', optionText: 'Mark dependency as @Injectable().' }
+ ],
+ answer: '3',
+ explanation: 'the first step is marking the class as @Injectable()',
+ selectedOption: ''
+ },
+ {
+ questionId: 4,
+ question: 'In which of the following does dependency injection occur?',
+ options: [
+ { optionValue: '1', optionText: '@Injectable()' },
+ { optionValue: '2', optionText: 'constructor' },
+ { optionValue: '3', optionText: 'function' },
+ { optionValue: '4', optionText: 'NgModule' },
+ ],
+ answer: '2',
+ explanation: 'object instantiations are taken care of by the constructor by Angular',
+ selectedOption: ''
+ },
+ {
+ questionId: 5,
+ question: 'Which access modifier is typically used in DI to make a service accessible in a class?',
+ options: [
+ { optionValue: '1', optionText: 'public' },
+ { optionValue: '2', optionText: 'protected' },
+ { optionValue: '3', optionText: 'private' },
+ { optionValue: '4', optionText: 'static' },
+ ],
+ answer: '3',
+ explanation: 'the private keyword, when used within the constructor, tells Angular that the service is accessible',
+ selectedOption: ''
+ },
+ {
+ questionId: 6,
+ question: 'How does Angular know that a service is available?',
+ options: [
+ { optionValue: '1', optionText: 'If listed in the constructor.' },
+ { optionValue: '2', optionText: 'If listed in the providers section of NgModule.' },
+ { optionValue: '3', optionText: 'If the service is declared as an interface.' },
+ { optionValue: '4', optionText: 'If the service is lazy-loaded.' },
+ ],
+ answer: '2',
+ explanation: 'Angular looks at the providers section of NgModule to locate services that are available',
+ selectedOption: ''
+ },
+ {
+ questionId: 7,
+ question: 'How does Angular avoid conflicts caused by using hardcoded strings as tokens?',
+ options: [
+ { optionValue: '1', optionText: 'Use an InjectionToken class' },
+ { optionValue: '2', optionText: 'Use @Inject()' },
+ { optionValue: '3', optionText: 'Use useFactory' },
+ { optionValue: '4', optionText: 'Use useValue' },
+ ],
+ answer: '1',
+ explanation: 'an InjectionToken class is preferable to using strings',
+ selectedOption: ''
+ },
+ {
+ questionId: 8,
+ question: 'Which is the preferred method for getting necessary data from a backend?',
+ options: [
+ { optionValue: '1', optionText: 'HttpClient' },
+ { optionValue: '2', optionText: 'WebSocket' },
+ { optionValue: '3', optionText: 'NgRx' },
+ { optionValue: '4', optionText: 'JSON' }
+ ],
+ answer: '1',
+ explanation: 'a server makes an HTTP request using the HttpClient service',
+ selectedOption: ''
+ },
+ {
+ questionId: 9,
+ question: 'In which of the following can Angular use services?',
+ options: [
+ { optionValue: '1', optionText: 'Lazy-loaded modules' },
+ { optionValue: '2', optionText: 'Eagerly loaded modules' },
+ { optionValue: '3', optionText: 'Feature modules' },
+ { optionValue: '4', optionText: 'All of the above.' },
+ ],
+ answer: '4',
+ explanation: 'Angular can utilize services with any of these methods',
+ selectedOption: ''
+ },
+ {
+ questionId: 10,
+ question: 'Which of the following is true concerning dependency injection?',
+ options: [
+ { optionValue: '1', optionText: 'It is a software design pattern.' },
+ { optionValue: '2', optionText: 'Injectors form a hierarchy.' },
+ { optionValue: '3', optionText: 'Providers register objects for future injection.' },
+ { optionValue: '4', optionText: 'All of the above.' }
+ ],
+ answer: '4',
+ explanation: 'all of these are correct statements about dependency injection',
+ selectedOption: ''
+ }
+ ];
+
+ constructor(private route: ActivatedRoute, private router: Router) {
+ this.route.paramMap.subscribe(params => {
+ // get the question ID and store it.
+ this.setQuestionID(+params.get('questionId'));
+ this.question = this.getQuestion;
+ });
+ }
+
+ ngOnInit() {
+ this.question = this.getQuestion;
+ this.totalQuestions = this.allQuestions.length;
+ this.timeLeft = this.timePerQuestion;
+ this.progressValue = 100 * (this.currentQuestion + 1) / this.totalQuestions;
+ this.countDown();
+ }
+
+ displayNextQuestionWithOptions() {
+ this.resetTimer();
+ this.increaseProgressValue();
+
+ this.questionIndex = this.questionID++;
+ document.getElementById('question').innerHTML = this.allQuestions[this.questionIndex].question;
+ document.getElementById('question').style.border = this.blueBorder;
+
+ for (this.optionIndex = 0; this.optionIndex < 4; this.optionIndex++) {
+ document.getElementsByTagName('li')[this.optionIndex].innerHTML =
+ this.allQuestions[this.questionIndex].options[this.optionIndex].optionText; // add option text for list items
+ }
+ }
+
+ displayPreviousQuestion() {
+ this.resetTimer();
+ this.decreaseProgressValue();
+
+ this.questionIndex = this.currentQuestion -= 1; // decrease the question index by 2 for previous question
+ document.getElementById('question').innerHTML = this.allQuestions[this.questionIndex].question;
+ document.getElementById('question').style.border = this.blueBorder;
+ }
+
+ navigateToNextQuestion(): void {
+ this.currentQuestion++;
+
+ if (this.isThereAnotherQuestion()) {
+ this.router.navigate(['/question', this.getQuestionID() + 1]); // navigates to the next question
+ this.displayNextQuestionWithOptions(); // displays the next question
+ }
+
+ this.resetTimer();
+ }
+
+ navigateToPreviousQuestion(): void {
+ this.currentQuestion--;
+ this.router.navigate(['/question', this.getQuestionID() - 1]); // navigates to the previous question
+ this.displayPreviousQuestion(); // display the previous question
+ }
+
+ // increase the correct answer count when the correct answer is selected
+ incrementCorrectAnswersCount() {
+ if (this.question && this.question.selectedOption === this.question.answer) {
+ this.correctAnswersCount++;
+ this.correctAnswer = true;
+ } else {
+ this.correctAnswer = false;
+ }
+ }
+
+ // checks whether the question is a valid question and is answered correctly
+ checkIfValidAndCorrect(): void {
+ if (this.question && this.currentQuestion <= this.totalQuestions &&
+ this.question.selectedOption === this.question.answer) {
+ this.incrementCorrectAnswersCount();
+ this.disabled = false;
+ this.elapsedTime = Math.floor(this.timePerQuestion - this.timeLeft);
+ this.elapsedTimes.push(this.elapsedTime);
+ this.quizDelay(3000);
+ this.navigateToNextQuestion();
+ }
+ }
+
+ // increase the progress value when the user presses the next button
+ increaseProgressValue() {
+ this.progressValue = 100 * (this.currentQuestion + 1) / this.totalQuestions;
+ }
+
+ // decrease the progress value when the user presses the previous button
+ decreaseProgressValue() {
+ this.progressValue = (100 / this.totalQuestions) * (this.getQuestionID() - 1);
+ }
+
+ // determine the percentage from amount of correct answers given and the total number of questions
+ calculatePercentage() {
+ this.percentage = 100 * (this.correctAnswersCount + 1) / this.totalQuestions;
+ }
+
+ recordSelections() {
+ if (this.question.selectedOption !== '') {
+ this.totalSelections++;
+ }
+ }
+
+ /**************** public API ***************/
+ getQuestionID() {
+ return this.questionID;
+ }
+
+ setQuestionID(id: number) {
+ return this.questionID = id;
+ }
+
+ isThereAnotherQuestion(): boolean {
+ return this.questionID <= this.allQuestions.length;
+ }
+
+ get getQuestion(): QuizQuestion {
+ return this.allQuestions.filter(
+ question => question.questionId === this.questionID
+ )[0];
+ }
+
+ // countdown timer and associated methods
+ private countDown() {
+ this.interval = setInterval(() => {
+ if (this.timeLeft > 0) {
+ this.timeLeft--;
+ this.recordSelections();
+
+ // utilized for disabling the next button until an option has been selected
+ if (this.question.selectedOption === '') {
+ this.disabled = true;
+ } else {
+ this.disabled = false;
+ }
+
+ if (this.question && this.currentQuestion <= this.totalQuestions && this.question.selectedOption !== null) {
+ this.totalQuestionsAttempted++;
+ }
+
+ this.checkIfValidAndCorrect();
+ this.calculatePercentage();
+ this.calculateTotalElapsedTime(this.elapsedTimes);
+
+ // check if the timer is expired
+ if (this.timeLeft === 0 && this.question && this.currentQuestion <= this.totalQuestions) {
+ this.question.questionId++;
+ this.displayNextQuestionWithOptions();
+ this.resetTimer();
+ }
+
+ if (this.question.questionId > this.totalQuestions) {
+ this.router.navigateByUrl('/results'); // todo: pass the data to results!
+ }
+ }
+ }, 1000);
+ }
+
+ private resetTimer() {
+ this.timeLeft = this.timePerQuestion;
+ }
+ private stopTimer() {
+ this.timeLeft = 0;
+ }
+
+ private calculateTotalElapsedTime(elapsedTimes) {
+ this.completionTime = elapsedTimes.reduce((acc, cur) => acc + cur, 0);
+ }
+
+ quizDelay(milliseconds) {
+ const start = new Date().getTime();
+ let counter = 0;
+ let end = 0;
+
+ while (counter < milliseconds) {
+ end = new Date().getTime();
+ counter = end - start;
+ }
+ }
+}
diff --git a/apps/playground/quiz/src/app/containers/results/results.component.html b/apps/playground/quiz/src/app/containers/results/results.component.html
new file mode 100644
index 000000000..8c21750cc
--- /dev/null
+++ b/apps/playground/quiz/src/app/containers/results/results.component.html
@@ -0,0 +1,93 @@
+
+
+
+ Dependency Injection Quiz
+
Results
+
+
+
+
+
+
Statistics
+ You answered {{ correctAnswersCount }} out of {{ totalQuestions }} questions correctly.
+ You completed the quiz in {{ elapsedMinutes }} minutes and {{ elapsedSeconds }} seconds.
+
+
+
+
+
diff --git a/apps/playground/quiz/src/app/containers/results/results.component.scss b/apps/playground/quiz/src/app/containers/results/results.component.scss
new file mode 100644
index 000000000..7a5d6f16d
--- /dev/null
+++ b/apps/playground/quiz/src/app/containers/results/results.component.scss
@@ -0,0 +1,167 @@
+$font-stack: Space Mono, monospace;
+$font-weight-max: 900;
+
+section.results {
+ img {
+ width: 150px;
+ height: 150px;
+ margin: 0 auto !important;
+ display: flex;
+ justify-content: center;
+ text-align: center;
+ }
+ h3, p {
+ text-align: center;
+ }
+
+ section.statistics {
+ h3, span, div.quiz-feedback, div span {
+ text-align: center;
+ }
+
+ span {
+ display: block;
+ }
+
+ div.quiz-feedback {
+ margin-top: 20px;
+ }
+ }
+
+ section.quizSummary {
+ display: flex;
+ flex-direction: column;
+
+ details {
+ text-align: center !important;
+
+ summary {
+ font-size: 16px;
+ color: #007aff;
+ font-weight: $font-weight-max;
+ }
+
+ .quiz-summary-question {
+ font-family: $font-stack;
+ font-size: 14px;
+ border: 2px solid #385d8a;
+ border-radius: 5px;
+ color: black;
+ margin-bottom: 15px;
+ padding: 10px;
+ text-align: left;
+
+ .quiz-summary-field {
+ display: block !important;
+ margin-bottom: 10px;
+ text-align: left;
+
+ span {
+ display: inline !important;
+ text-align: left;
+ font-size: 16px;
+ color: #00008b;
+ }
+ span.leader {
+ font-weight: $font-weight-max;
+ }
+
+ mat-icon {
+ font-size: larger;
+ top: 10px !important;
+ }
+ mat-icon.correct {
+ color: #006400 !important;
+ font-weight: $font-weight-max;
+ }
+ mat-icon.incorrect {
+ color: #ff0000 !important;
+ font-weight: $font-weight-max;
+ }
+ }
+ }
+ }
+ }
+}
+
+
+section.return {
+ text-align: center;
+ margin-top: -10px;
+
+ a.btn {
+ margin-right: 15px;
+ background-color: #20b2aa;
+ color: white;
+ border: 1px solid black;
+ border-radius: 5px;
+ }
+}
+
+section.challenge-social {
+ text-align: center;
+
+ a.btn {
+ text-decoration: none;
+ margin-right: 20px;
+ padding: 9px 15px 8px 42px;
+ background: no-repeat 10px 5px;
+ background-size: 25px 25px;
+ color: white;
+ }
+ a.btn.facebook {
+ background-color: #4267b2;
+ background-image: url('../../../assets/images/facebook.gif');
+ top: 5px;
+ }
+ a.btn.twitter {
+ background-color: #59adeb;
+ background-image: url('../../../assets/images/twitter.svg');
+ top: 5px;
+ }
+ a.btn.email {
+ background-color: #f0a121;
+ background-image: url('../../../assets/images/email.svg');
+ top: 5px;
+ }
+}
+
+/* add ripple effect to buttons on ResultsComponent */
+a.btn {
+ position: relative;
+ overflow: hidden;
+}
+
+a.btn::after {
+ display: none;
+ content: "";
+ position: absolute;
+ border-radius: 50%;
+ background-color: rgba(0, 0, 0, 0.3);
+
+ width: 100px;
+ height: 100px;
+ margin-top: -50px;
+ margin-left: -50px;
+
+ /* Center the ripple */
+ top: 50%;
+ left: 50%;
+
+ animation: ripple 1s;
+ opacity: 0;
+}
+a.btn:focus:not(:active)::after {
+ display: block;
+}
+
+@keyframes ripple {
+ from {
+ opacity: 1;
+ transform: scale(0);
+ }
+ to {
+ opacity: 0;
+ transform: scale(10);
+ }
+}
diff --git a/apps/playground/quiz/src/app/containers/results/results.component.ts b/apps/playground/quiz/src/app/containers/results/results.component.ts
new file mode 100644
index 000000000..b3c98c886
--- /dev/null
+++ b/apps/playground/quiz/src/app/containers/results/results.component.ts
@@ -0,0 +1,45 @@
+import { Component, OnInit, Input } from '@angular/core';
+
+import { QuizQuestion } from '../../model/QuizQuestion';
+
+@Component({
+ selector: 'codelab-quiz-results',
+ templateUrl: './results.component.html',
+ styleUrls: ['./results.component.scss']
+})
+export class ResultsComponent implements OnInit {
+ @Input() question: QuizQuestion;
+ @Input() allQuestions: QuizQuestion[];
+ @Input() totalQuestions: number;
+ @Input() totalQuestionsAttempted: number;
+ @Input() correctAnswersCount: number;
+ @Input() percentage: number;
+ @Input() completionTime: number;
+ @Input() totalSelections: number;
+
+ elapsedMinutes: number;
+ elapsedSeconds: number;
+
+ ANGULAR_TROPHY = '../../../assets/images/ng-trophy.png';
+ NOT_BAD = '../../../assets/images/not-bad.jpg';
+ TRY_AGAIN = '../../../assets/images/try-again.jpeg';
+ codelabUrl = 'https://www.codelab.fun';
+
+ constructor() {}
+
+ ngOnInit() {
+ if (this.percentage < 0) {
+ this.percentage = 0;
+ }
+ if (this.percentage > 100) {
+ this.percentage = 100;
+ }
+
+ if (this.correctAnswersCount > this.totalQuestions) {
+ this.correctAnswersCount = this.totalQuestions;
+ }
+
+ this.elapsedMinutes = Math.floor(this.completionTime / 60);
+ this.elapsedSeconds = this.completionTime % 60;
+ }
+}
diff --git a/apps/playground/quiz/src/app/model/Option.ts b/apps/playground/quiz/src/app/model/Option.ts
new file mode 100644
index 000000000..46f75a2aa
--- /dev/null
+++ b/apps/playground/quiz/src/app/model/Option.ts
@@ -0,0 +1,4 @@
+export interface Option {
+ optionValue: string;
+ optionText: string;
+}
diff --git a/apps/playground/quiz/src/app/model/QuizQuestion.ts b/apps/playground/quiz/src/app/model/QuizQuestion.ts
new file mode 100644
index 000000000..7acde9151
--- /dev/null
+++ b/apps/playground/quiz/src/app/model/QuizQuestion.ts
@@ -0,0 +1,10 @@
+import { Option } from './Option';
+
+export interface QuizQuestion {
+ questionId: number;
+ question: string;
+ options: Option[];
+ answer: string;
+ explanation: string;
+ selectedOption: string;
+}
diff --git a/apps/playground/quiz/src/assets/DjbUpOnTheScoreboard.ttf b/apps/playground/quiz/src/assets/DjbUpOnTheScoreboard.ttf
new file mode 100644
index 000000000..ded21514c
Binary files /dev/null and b/apps/playground/quiz/src/assets/DjbUpOnTheScoreboard.ttf differ
diff --git a/apps/playground/quiz/src/assets/alarm-clock.ttf b/apps/playground/quiz/src/assets/alarm-clock.ttf
new file mode 100644
index 000000000..9e9b59345
Binary files /dev/null and b/apps/playground/quiz/src/assets/alarm-clock.ttf differ
diff --git a/apps/playground/quiz/src/assets/images/angular.png b/apps/playground/quiz/src/assets/images/angular.png
new file mode 100644
index 000000000..6c115fba8
Binary files /dev/null and b/apps/playground/quiz/src/assets/images/angular.png differ
diff --git a/apps/playground/quiz/src/assets/images/angular.svg b/apps/playground/quiz/src/assets/images/angular.svg
new file mode 100644
index 000000000..bf081acb1
--- /dev/null
+++ b/apps/playground/quiz/src/assets/images/angular.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/apps/playground/quiz/src/assets/images/congratulations.jpg b/apps/playground/quiz/src/assets/images/congratulations.jpg
new file mode 100644
index 000000000..06bcb7b38
Binary files /dev/null and b/apps/playground/quiz/src/assets/images/congratulations.jpg differ
diff --git a/apps/playground/quiz/src/assets/images/dependency-injection-diagram.png b/apps/playground/quiz/src/assets/images/dependency-injection-diagram.png
new file mode 100644
index 000000000..8aab83cbc
Binary files /dev/null and b/apps/playground/quiz/src/assets/images/dependency-injection-diagram.png differ
diff --git a/apps/playground/quiz/src/assets/images/email.svg b/apps/playground/quiz/src/assets/images/email.svg
new file mode 100644
index 000000000..77c5db3ca
--- /dev/null
+++ b/apps/playground/quiz/src/assets/images/email.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/apps/playground/quiz/src/assets/images/facebook.gif b/apps/playground/quiz/src/assets/images/facebook.gif
new file mode 100644
index 000000000..cb02b6d11
Binary files /dev/null and b/apps/playground/quiz/src/assets/images/facebook.gif differ
diff --git a/apps/playground/quiz/src/assets/images/headgears.png b/apps/playground/quiz/src/assets/images/headgears.png
new file mode 100644
index 000000000..a0b92b19d
Binary files /dev/null and b/apps/playground/quiz/src/assets/images/headgears.png differ
diff --git a/apps/playground/quiz/src/assets/images/ng-trophy.png b/apps/playground/quiz/src/assets/images/ng-trophy.png
new file mode 100644
index 000000000..282d7c434
Binary files /dev/null and b/apps/playground/quiz/src/assets/images/ng-trophy.png differ
diff --git a/apps/playground/quiz/src/assets/images/not-bad.jpg b/apps/playground/quiz/src/assets/images/not-bad.jpg
new file mode 100644
index 000000000..e8a2f9f31
Binary files /dev/null and b/apps/playground/quiz/src/assets/images/not-bad.jpg differ
diff --git a/apps/playground/quiz/src/assets/images/try-again.jpeg b/apps/playground/quiz/src/assets/images/try-again.jpeg
new file mode 100644
index 000000000..9b6fdaa1e
Binary files /dev/null and b/apps/playground/quiz/src/assets/images/try-again.jpeg differ
diff --git a/apps/playground/quiz/src/assets/images/twitter.svg b/apps/playground/quiz/src/assets/images/twitter.svg
new file mode 100644
index 000000000..a4ed81154
--- /dev/null
+++ b/apps/playground/quiz/src/assets/images/twitter.svg
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/apps/playground/quiz/src/environments/environment.prod.ts b/apps/playground/quiz/src/environments/environment.prod.ts
new file mode 100644
index 000000000..3612073bc
--- /dev/null
+++ b/apps/playground/quiz/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: true
+};
diff --git a/apps/playground/quiz/src/environments/environment.ts b/apps/playground/quiz/src/environments/environment.ts
new file mode 100644
index 000000000..7b4f817ad
--- /dev/null
+++ b/apps/playground/quiz/src/environments/environment.ts
@@ -0,0 +1,16 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false
+};
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
diff --git a/apps/playground/quiz/src/favicon.ico b/apps/playground/quiz/src/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/playground/quiz/src/favicon.ico differ
diff --git a/apps/playground/quiz/src/index.html b/apps/playground/quiz/src/index.html
new file mode 100644
index 000000000..f266651f1
--- /dev/null
+++ b/apps/playground/quiz/src/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Angular/TypeScript Codelab Quiz
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/playground/quiz/src/main.ts b/apps/playground/quiz/src/main.ts
new file mode 100644
index 000000000..fa4e0aef3
--- /dev/null
+++ b/apps/playground/quiz/src/main.ts
@@ -0,0 +1,13 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic()
+ .bootstrapModule(AppModule)
+ .catch(err => console.error(err));
diff --git a/apps/playground/quiz/src/polyfills.ts b/apps/playground/quiz/src/polyfills.ts
new file mode 100644
index 000000000..2f258e56c
--- /dev/null
+++ b/apps/playground/quiz/src/polyfills.ts
@@ -0,0 +1,62 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ * because those flags need to be set before `zone.js` being loaded, and webpack
+ * will put import in the top of bundle, so user need to create a separate file
+ * in this directory (for example: zone-flags.ts), and put the following flags
+ * into that file, and then add the following code before importing zone.js.
+ * import './zone-flags.ts';
+ *
+ * The flags allowed in zone-flags.ts are listed here.
+ *
+ * The following flags will work for all browsers.
+ *
+ * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+ *
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ *
+ * (window as any).__Zone_enable_cross_context_check = true;
+ *
+ */
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/apps/playground/quiz/src/styles.scss b/apps/playground/quiz/src/styles.scss
new file mode 100644
index 000000000..f2a223ac2
--- /dev/null
+++ b/apps/playground/quiz/src/styles.scss
@@ -0,0 +1,56 @@
+@import "../../../../node_modules/@angular/material/prebuilt-themes/indigo-pink.css";
+$font-stack: Space Mono, monospace;
+$font-weight-max: 900;
+
+mat-card {
+ margin: 0 auto;
+ margin-bottom: 20px;
+ margin-top: 5%;
+ margin-left: 25%;
+ width: 42rem;
+ height: inherit;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ position: relative;
+ border: 1px solid black;
+ border-radius: 10px !important;
+}
+mat-card:hover {
+ box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2) !important;
+ transition: 0.3s !important;
+}
+
+mat-card-header {
+ text-align: center;
+ display: flex;
+ justify-content: center;
+
+ .header-image {
+ background-image: url('assets/images/angular.png');
+ background-size: cover;
+ margin-left: -10px;
+ margin-top: -10px;
+ height: 100px !important;
+ width: 100px !important;
+ }
+
+ mat-card-title {
+ font-family: $font-stack;
+ font-weight: $font-weight-max;
+ font-size: 30px !important;
+ margin: -10px 0 10px 10px;
+ color: #007aff;
+ text-align: center;
+ }
+ mat-card-subtitle {
+ font-family: $font-stack;
+ font-weight: $font-weight-max;
+ font-size: 17.5px !important;
+ font-style: italic;
+ color: #808080;
+ text-align: center;
+ }
+}
+
diff --git a/apps/playground/quiz/src/test-setup.ts b/apps/playground/quiz/src/test-setup.ts
new file mode 100644
index 000000000..8d88704e8
--- /dev/null
+++ b/apps/playground/quiz/src/test-setup.ts
@@ -0,0 +1 @@
+import 'jest-preset-angular';
diff --git a/apps/playground/quiz/tsconfig.app.json b/apps/playground/quiz/tsconfig.app.json
new file mode 100644
index 000000000..8925f33e8
--- /dev/null
+++ b/apps/playground/quiz/tsconfig.app.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": []
+ },
+ "files": ["src/main.ts", "src/polyfills.ts"],
+ "include": ["**/*.ts"],
+ "exclude": ["src/test-setup.ts", "**/*.spec.ts"]
+}
diff --git a/apps/playground/quiz/tsconfig.json b/apps/playground/quiz/tsconfig.json
new file mode 100644
index 000000000..08c7db8c9
--- /dev/null
+++ b/apps/playground/quiz/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "types": ["node", "jest"]
+ },
+ "include": ["**/*.ts"]
+}
diff --git a/apps/playground/quiz/tsconfig.spec.json b/apps/playground/quiz/tsconfig.spec.json
new file mode 100644
index 000000000..fd405a65e
--- /dev/null
+++ b/apps/playground/quiz/tsconfig.spec.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": ["**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/apps/playground/quiz/tslint.json b/apps/playground/quiz/tslint.json
new file mode 100644
index 000000000..b6ad5c3a5
--- /dev/null
+++ b/apps/playground/quiz/tslint.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tslint.json",
+ "rules": {
+ "directive-selector": [true, "attribute", "codelab", "camelCase"],
+ "component-selector": [true, "element", "codelab", "kebab-case"]
+ }
+}
diff --git a/quiz/browserslist b/quiz/browserslist
new file mode 100644
index 000000000..80848532e
--- /dev/null
+++ b/quiz/browserslist
@@ -0,0 +1,12 @@
+# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+
+# You can see what browsers were selected by your queries by running:
+# npx browserslist
+
+> 0.5%
+last 2 versions
+Firefox ESR
+not dead
+not IE 9-11 # For IE 9-11 support, remove 'not'.
\ No newline at end of file
diff --git a/quiz/jest.config.js b/quiz/jest.config.js
new file mode 100644
index 000000000..12b435256
--- /dev/null
+++ b/quiz/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ name: 'playground-quiz',
+ preset: '../../../jest.config.js',
+ coverageDirectory: '../../../coverage/apps/playground/quiz',
+ snapshotSerializers: [
+ 'jest-preset-angular/AngularSnapshotSerializer.js',
+ 'jest-preset-angular/HTMLCommentSerializer.js'
+ ]
+};
diff --git a/quiz/src/app/app-routing.module.ts b/quiz/src/app/app-routing.module.ts
new file mode 100644
index 000000000..cc1581282
--- /dev/null
+++ b/quiz/src/app/app-routing.module.ts
@@ -0,0 +1,21 @@
+import { NgModule } from '@angular/core';
+import { Route, RouterModule } from '@angular/router';
+import { IntroductionComponent } from './containers/introduction/introduction.component';
+import { QuestionComponent } from './containers/question/question.component';
+import { ResultsComponent } from './containers/results/results.component';
+
+const routes: Route[] = [
+ { path: 'intro', component: IntroductionComponent, pathMatch: 'full' },
+ { path: 'question', component: QuestionComponent, pathMatch: 'full' },
+ { path: 'question/:questionId', component: QuestionComponent, pathMatch: 'full' },
+ { path: 'results', component: ResultsComponent, pathMatch: 'full' },
+ { path: '', redirectTo: 'intro', pathMatch: 'full' }
+];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes, {
+ // enableTracing: true
+ })],
+ exports: [RouterModule]
+})
+export class AppRoutingModule { }
diff --git a/quiz/src/app/app.component.html b/quiz/src/app/app.component.html
new file mode 100644
index 000000000..6c46b1de8
--- /dev/null
+++ b/quiz/src/app/app.component.html
@@ -0,0 +1 @@
+
diff --git a/quiz/src/app/app.component.scss b/quiz/src/app/app.component.scss
new file mode 100644
index 000000000..5401b8eb8
--- /dev/null
+++ b/quiz/src/app/app.component.scss
@@ -0,0 +1,133 @@
+/*
+ * Remove template code below
+ */
+:host {
+ display: block;
+ font-family: sans-serif;
+ min-width: 300px;
+ max-width: 1600px;
+ margin: 50px auto;
+}
+
+.gutter-left {
+ margin-left: 9px;
+}
+
+.col-span-2 {
+ grid-column: span 2;
+}
+
+.flex {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+header {
+ background-color: #143055;
+ color: white;
+ padding: 5px;
+ border-radius: 3px;
+}
+
+main {
+ padding: 0 36px;
+}
+
+p {
+ text-align: center;
+}
+
+h1 {
+ text-align: center;
+ margin-left: 18px;
+ font-size: 24px;
+}
+
+h2 {
+ text-align: center;
+ font-size: 20px;
+ margin: 40px 0 10px 0;
+}
+
+.resources {
+ text-align: center;
+ list-style: none;
+ padding: 0;
+ display: grid;
+ grid-gap: 9px;
+ grid-template-columns: 1fr 1fr;
+}
+
+.resource {
+ color: #0094ba;
+ height: 36px;
+ background-color: rgba(0, 0, 0, 0);
+ border: 1px solid rgba(0, 0, 0, 0.12);
+ border-radius: 4px;
+ padding: 3px 9px;
+ text-decoration: none;
+}
+
+.resource:hover {
+ background-color: rgba(68, 138, 255, 0.04);
+}
+
+pre {
+ padding: 9px;
+ border-radius: 4px;
+ background-color: black;
+ color: #eee;
+}
+
+details {
+ border-radius: 4px;
+ color: #333;
+ background-color: rgba(0, 0, 0, 0);
+ border: 1px solid rgba(0, 0, 0, 0.12);
+ padding: 3px 9px;
+ margin-bottom: 9px;
+}
+
+summary {
+ cursor: pointer;
+ outline: none;
+ height: 36px;
+ line-height: 36px;
+}
+
+.github-star-container {
+ margin-top: 12px;
+ line-height: 20px;
+}
+
+.github-star-container a {
+ display: flex;
+ align-items: center;
+ text-decoration: none;
+ color: #333;
+}
+
+.github-star-badge {
+ color: #24292e;
+ display: flex;
+ align-items: center;
+ font-size: 12px;
+ padding: 3px 10px;
+ border: 1px solid rgba(27, 31, 35, 0.2);
+ border-radius: 3px;
+ background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
+ margin-left: 4px;
+ font-weight: 600;
+}
+
+.github-star-badge:hover {
+ background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%);
+ border-color: rgba(27, 31, 35, 0.35);
+ background-position: -0.5em;
+}
+.github-star-badge .material-icons {
+ height: 16px;
+ width: 16px;
+ margin-right: 4px;
+}
diff --git a/quiz/src/app/app.component.spec.ts b/quiz/src/app/app.component.spec.ts
new file mode 100644
index 000000000..d93819206
--- /dev/null
+++ b/quiz/src/app/app.component.spec.ts
@@ -0,0 +1,35 @@
+import { TestBed, async } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { AppComponent } from './app.component';
+
+describe('AppComponent', () => {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule
+ ],
+ declarations: [
+ AppComponent
+ ],
+ }).compileComponents();
+ }));
+
+ it('should create the app', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ expect(app).toBeTruthy();
+ });
+
+ it(`should have as title 'quiz'`, () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ expect(app.title).toEqual('quiz');
+ });
+
+ it('should render title', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.nativeElement;
+ expect(compiled.querySelector('.content span').textContent).toContain('quiz app is running!');
+ });
+});
diff --git a/quiz/src/app/app.component.ts b/quiz/src/app/app.component.ts
new file mode 100644
index 000000000..7f4f16ed5
--- /dev/null
+++ b/quiz/src/app/app.component.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'codelab-quiz',
+ templateUrl: './app.component.html',
+ styleUrls: [ './app.component.css' ]
+})
+export class AppComponent {
+ name = 'Angular';
+}
diff --git a/quiz/src/app/app.module.ts b/quiz/src/app/app.module.ts
new file mode 100644
index 000000000..e46bffd5b
--- /dev/null
+++ b/quiz/src/app/app.module.ts
@@ -0,0 +1,48 @@
+import { NgModule, NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatCardModule } from '@angular/material/card';
+import { MatRadioModule, MAT_RADIO_DEFAULT_OPTIONS } from '@angular/material/radio';
+import { MatIconModule } from '@angular/material/icon';
+import { MatButtonModule } from '@angular/material/button';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import { IntroductionComponent } from './containers/introduction/introduction.component';
+import { QuestionComponent } from './containers/question/question.component';
+import * as QuestionComponent2 from './components/question/question.component';
+import { ResultsComponent } from './containers/results/results.component';
+
+
+@NgModule({
+ declarations: [
+ AppComponent,
+ IntroductionComponent,
+ QuestionComponent,
+ QuestionComponent2.QuestionComponent,
+ ResultsComponent
+ ],
+ imports: [
+ BrowserModule,
+ BrowserAnimationsModule,
+ AppRoutingModule,
+ ReactiveFormsModule,
+ MatCardModule,
+ MatRadioModule,
+ MatIconModule,
+ MatButtonModule,
+ NgbModule
+ ],
+ providers: [{
+ provide: MAT_RADIO_DEFAULT_OPTIONS,
+ useValue: { color: 'accent' },
+ }],
+ bootstrap: [ AppComponent ],
+ schemas: [
+ CUSTOM_ELEMENTS_SCHEMA,
+ NO_ERRORS_SCHEMA
+ ]
+})
+export class AppModule { }
diff --git a/quiz/src/app/components/question/question.component.html b/quiz/src/app/components/question/question.component.html
new file mode 100644
index 000000000..a328af71b
--- /dev/null
+++ b/quiz/src/app/components/question/question.component.html
@@ -0,0 +1,28 @@
+
+
+
diff --git a/quiz/src/app/components/question/question.component.scss b/quiz/src/app/components/question/question.component.scss
new file mode 100644
index 000000000..e5a9874f8
--- /dev/null
+++ b/quiz/src/app/components/question/question.component.scss
@@ -0,0 +1,99 @@
+$font-stack: Space Mono, monospace;
+$font-weight-max: 900;
+
+ol {
+ margin-top: 15px;
+ margin-left: -40px;
+ cursor: pointer;
+}
+ol li {
+ margin-left: 30px;
+}
+
+.radio-options {
+ margin-bottom: 5px;
+ margin-left: 0.5rem;
+ padding: 4px;
+}
+
+.option {
+ border: 2px solid #979797;
+ font-family: $font-stack;
+ font-size: 20px;
+ color: #0f0900;
+ background-color: #f5f5f5;
+ width: 39rem !important;
+ height: auto;
+ padding: 5px 5px 0 30px;
+ margin-left: -5px;
+ vertical-align: middle;
+}
+.option:hover {
+ outline: 2px solid #007aff;
+}
+
+section.messages {
+ display: flex;
+ justify-content: center;
+
+ .message {
+ font-family: $font-stack;
+ font-weight: $font-weight-max;
+ font-size: 16px;
+ font-style: italic;
+ text-align: center !important;
+ margin: 10px 0 0 0;
+ padding: 10px !important;
+ width: 32rem !important;
+ display: inline-flex;
+ align-items: center;
+ vertical-align: middle;
+ justify-content: center !important;
+ margin: 0 auto !important;
+ margin-top: 10px !important;
+ }
+ .correct-message {
+ font-weight: $font-weight-max;
+ font-style: italic;
+ border: 2px solid #007aff;
+ border-radius: 5px;
+ color: #00c853 !important;
+ }
+ .wrong-message {
+ font-weight: $font-weight-max;
+ font-style: italic;
+ border: 2px solid #ff0000;
+ border-radius: 5px;
+ color: #ff0000 !important;
+ padding: 5px;
+ }
+ mat-icon.sentiment {
+ font-size: 30px !important;
+ color: #9acd32;
+ margin-right: -50px !important;
+ vertical-align: top;
+ margin-top: 18px;
+ }
+ pre {
+ font-size: 17px;
+ margin-top: 10px;
+ }
+}
+
+::ng-deep .mat-radio-button .mat-radio-container {
+ display: none;
+}
+
+.feedback-icon {
+ position: absolute;
+ right: 0;
+ margin-right: 40px;
+ margin-top: -25px;
+}
+
+.is-correct {
+ background-color: #00c853 !important;
+}
+.is-incorrect {
+ background-color: #ff0000 !important;
+}
diff --git a/quiz/src/app/components/question/question.component.ts b/quiz/src/app/components/question/question.component.ts
new file mode 100644
index 000000000..7671b7e2e
--- /dev/null
+++ b/quiz/src/app/components/question/question.component.ts
@@ -0,0 +1,64 @@
+import { Component, OnInit, OnChanges, SimpleChanges, Input, Output, EventEmitter } from '@angular/core';
+import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';
+
+import { QuizQuestion } from '../../model/QuizQuestion';
+
+@Component({
+ selector: 'codelab-quiz-question',
+ templateUrl: './question.component.html',
+ styleUrls: ['./question.component.scss']
+})
+export class QuestionComponent implements OnInit, OnChanges {
+ @Output() answer = new EventEmitter();
+ @Output() formGroup: FormGroup;
+ @Input() question: QuizQuestion;
+ @Input() allQuestions: QuizQuestion[];
+ @Input() totalQuestions: number;
+ option = '';
+ selectedOption = '';
+ grayBorder = '2px solid #979797';
+
+ constructor(private fb: FormBuilder) {}
+
+ ngOnInit() {
+ this.buildForm();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.question && changes.question.currentValue && !changes.question.firstChange) {
+ this.formGroup.patchValue({answer: ''});
+ }
+ }
+
+ private buildForm() {
+ this.formGroup = this.fb.group({
+ answer: new FormControl(['', Validators.required])
+ });
+ }
+
+ radioChange(answer: string) {
+ this.question.selectedOption = answer;
+ this.answer.emit(answer);
+ this.displayExplanation();
+ }
+
+ displayExplanation(): void {
+ document.getElementById('question').innerHTML =
+ 'Option ' + this.question.answer + ' was correct because ' + this.question.explanation + '.';
+ document.getElementById('question').style.border = this.grayBorder;
+ }
+
+ isCorrect(option: string): boolean {
+ // mark the correct answer regardless of which option is selected once answered
+ return this.question.selectedOption && option === this.question.answer;
+ }
+
+ isIncorrect(option: string): boolean {
+ // mark incorrect answer if selected
+ return option !== this.question.answer && option === this.question.selectedOption;
+ }
+
+ onSubmit() {
+ this.formGroup.reset({answer: null});
+ }
+}
diff --git a/quiz/src/app/containers/introduction/introduction.component.html b/quiz/src/app/containers/introduction/introduction.component.html
new file mode 100644
index 000000000..86c463776
--- /dev/null
+++ b/quiz/src/app/containers/introduction/introduction.component.html
@@ -0,0 +1,17 @@
+
+
+
+ Dependency Injection Quiz
+ How well do you know Dependency Injection?
+ Take the quiz and find out!
+
+
+
+
Take this awesome quiz that will help improve your understanding of Dependency Injection. The timed questionnaire
+ with automatic scoring provides you with a final score at the end. Match wits with your friends! Practice to
+ increase your knowledge. Good luck and have fun with this quiz. Share and enjoy!
+ Question {{ question.questionId }} of {{ totalQuestions }}
+
+
+ Time
+
+ 0:0{{ timeLeft }}
+
+
+
+
+
+
+
{{ question.question }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = 0 && progressValue <= 100) &&
+ question && question.questionId <= totalQuestions"
+ type="success" [striped]="true" [animated]="true" [value]="progressValue">
+ {{ progressValue.toFixed(0) }}%
+
+
+
+
+
+
+
+
+
diff --git a/quiz/src/app/containers/question/question.component.scss b/quiz/src/app/containers/question/question.component.scss
new file mode 100644
index 000000000..058c15297
--- /dev/null
+++ b/quiz/src/app/containers/question/question.component.scss
@@ -0,0 +1,136 @@
+$font-stack: Space Mono, monospace;
+$font-weight-max: 900;
+
+@font-face {
+ font-family: "Alarm Clock";
+ src: url("../../../assets/alarm-clock.ttf") format("truetype");
+}
+
+section.scoreboard {
+ margin-top: 10px !important;
+
+ .row {
+ display: inline;
+ }
+ .score {
+ float: left;
+ margin-left: 1rem;
+ }
+ .score .leader {
+ margin-left: 15px;
+ }
+ .badge {
+ float: left;
+ margin: 20px 10px 0 100px;
+ font-family: $font-stack;
+ font-size: 24px;
+ font-weight: $font-weight-max;
+ font-style: italic;
+ }
+ .time-left {
+ float: right;
+ margin-right: 1rem;
+ }
+ .time-left .leader {
+ margin-left: 20px;
+ }
+ .scoreboard {
+ font-family: "Alarm Clock", $font-stack;
+ font-weight: $font-weight-max;
+ font-size: 30px;
+ color: #006400;
+ display: inline-block;
+ margin: -5px 0 0 15px;
+ width: auto;
+ }
+ .leader {
+ display: block;
+ font-weight: $font-weight-max;
+ font-size: 18px;
+ text-transform: uppercase;
+ position: relative;
+ top: -5px;
+ }
+}
+
+section.time-expired {
+ margin: 50px 0 20px 0 !important;
+ display: flex;
+ justify-content: center;
+
+ button.time-expired-btn {
+ font-family: $font-stack;
+ font-weight: $font-weight-max;
+ text-align: center;
+ font-style: italic;
+ color: #006400 !important;
+ width: 26.5rem;
+ border: 1px solid #ff0000;
+ border-radius: 5px;
+ padding: 5px;
+ margin-bottom: 20px;
+ }
+ .timer-expired-icon, .qa-icon {
+ font-weight: $font-weight-max;
+ font-size: 30px !important;
+ color: #9acd32;
+ }
+ span.proceed, span.viewResults {
+ margin-top: -30px;
+ }
+}
+
+#question {
+ font-family: $font-stack;
+ font-weight: 700;
+ font-size: 30px !important;
+ margin: 0 0 10px 0.4rem;
+ float: left;
+ border: 2px solid #007aff;
+ padding: 5px 10px 15px 20px;
+ background-color: #f5f5f5;
+ color: #0f0900;
+ width: 39rem !important;
+ height: auto;
+ vertical-align: middle;
+}
+
+section.paging {
+ width: 40rem;
+
+ mat-card-actions {
+ margin: -10px 0 10px 0;
+
+ .previousQuestionNav {
+ float: left;
+ margin-left: 1.5rem;
+ }
+ .nextQuestionNav {
+ float: right;
+ margin-right: -0.35rem;
+ }
+ .previousQuestionNav:hover, .nextQuestionNav:hover {
+ border: 1px solid #007aff;
+ }
+ }
+}
+
+section.progress-bar {
+ margin: 40px 0 10px 1.5rem;
+ width: 39rem;
+ height: auto;
+
+ ngb-progressbar {
+ border-radius: 10px;
+ }
+
+ .progress-note {
+ color: #ffff00;
+ font-family: $font-stack;
+ font-weight: $font-weight-max;
+ font-style: italic;
+ font-size: 20px;
+ padding-top: 5px;
+ margin-top: 5px;
+ }
+}
diff --git a/quiz/src/app/containers/question/question.component.ts b/quiz/src/app/containers/question/question.component.ts
new file mode 100644
index 000000000..176dbc08b
--- /dev/null
+++ b/quiz/src/app/containers/question/question.component.ts
@@ -0,0 +1,344 @@
+import { Component, OnInit, Input, Output } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { FormGroup } from '@angular/forms';
+
+import { QuizQuestion } from '../../model/QuizQuestion';
+
+@Component({
+ selector: 'codelab-question-container',
+ templateUrl: './question.component.html',
+ styleUrls: ['./question.component.scss']
+})
+export class QuestionComponent implements OnInit {
+ @Input() formGroup: FormGroup;
+ @Output() question: QuizQuestion;
+ @Output() totalQuestions: number;
+ @Output() totalSelections = 0;
+ @Output() totalQuestionsAttempted = 0;
+ @Output() correctAnswersCount = 0;
+ @Output() percentage = 0;
+ @Output() completionTime: number;
+
+ questionID = 0;
+ currentQuestion = 0;
+ questionIndex: number;
+ optionIndex: number;
+ correctAnswer: boolean;
+ disabled: boolean;
+ progressValue: number;
+ timeLeft: number;
+ timePerQuestion = 20;
+ interval: any;
+ elapsedTime: number;
+ elapsedTimes = [];
+ blueBorder = '2px solid #007aff';
+
+ @Output() allQuestions: QuizQuestion[] = [
+ {
+ questionId: 1,
+ question: 'What is the objective of dependency injection?',
+ options: [
+ { optionValue: '1', optionText: 'Pass the service to the client.' },
+ { optionValue: '2', optionText: 'Allow the client to find service.' },
+ { optionValue: '3', optionText: 'Allow the client to build service.' },
+ { optionValue: '4', optionText: 'Give the client part service.' }
+ ],
+ answer: '1',
+ explanation: 'a service gets passed to the client during DI',
+ selectedOption: ''
+ },
+ {
+ questionId: 2,
+ question: 'Which of the following benefit from dependency injection?',
+ options: [
+ { optionValue: '1', optionText: 'Programming' },
+ { optionValue: '2', optionText: 'Testability' },
+ { optionValue: '3', optionText: 'Software design' },
+ { optionValue: '4', optionText: 'All of the above.' },
+ ],
+ answer: '4',
+ explanation: 'DI simplifies both programming and testing as well as being a popular design pattern',
+ selectedOption: ''
+ },
+ {
+ questionId: 3,
+ question: 'Which of the following is the first step in setting up dependency injection?',
+ options: [
+ { optionValue: '1', optionText: 'Require in the component.' },
+ { optionValue: '2', optionText: 'Provide in the module.' },
+ { optionValue: '3', optionText: 'Mark dependency as @Injectable().' }
+ ],
+ answer: '3',
+ explanation: 'the first step is marking the class as @Injectable()',
+ selectedOption: ''
+ },
+ {
+ questionId: 4,
+ question: 'In which of the following does dependency injection occur?',
+ options: [
+ { optionValue: '1', optionText: '@Injectable()' },
+ { optionValue: '2', optionText: 'constructor' },
+ { optionValue: '3', optionText: 'function' },
+ { optionValue: '4', optionText: 'NgModule' },
+ ],
+ answer: '2',
+ explanation: 'object instantiations are taken care of by the constructor by Angular',
+ selectedOption: ''
+ }/*,
+ {
+ questionId: 5,
+ question: 'Which access modifier is typically used in DI to make a service accessible in a class?',
+ options: [
+ { optionValue: '1', optionText: 'public' },
+ { optionValue: '2', optionText: 'protected' },
+ { optionValue: '3', optionText: 'private' },
+ { optionValue: '4', optionText: 'static' },
+ ],
+ answer: '3',
+ explanation: 'the private keyword, when used within the constructor, tells Angular that the service is accessible',
+ selectedOption: ''
+ },
+ {
+ questionId: 6,
+ question: 'How does Angular know that a service is available?',
+ options: [
+ { optionValue: '1', optionText: 'If listed in the constructor.' },
+ { optionValue: '2', optionText: 'If listed in the providers section of NgModule.' },
+ { optionValue: '3', optionText: 'If the service is declared as an interface.' },
+ { optionValue: '4', optionText: 'If the service is lazy-loaded.' },
+ ],
+ answer: '2',
+ explanation: 'Angular looks at the providers section of NgModule to locate services that are available',
+ selectedOption: ''
+ },
+ {
+ questionId: 7,
+ question: 'How does Angular avoid conflicts caused by using hardcoded strings as tokens?',
+ options: [
+ { optionValue: '1', optionText: 'Use an InjectionToken class' },
+ { optionValue: '2', optionText: 'Use @Inject()' },
+ { optionValue: '3', optionText: 'Use useFactory' },
+ { optionValue: '4', optionText: 'Use useValue' },
+ ],
+ answer: '1',
+ explanation: 'an InjectionToken class is preferable to using strings',
+ selectedOption: ''
+ },
+ {
+ questionId: 8,
+ question: 'Which is the preferred method for getting necessary data from a backend?',
+ options: [
+ { optionValue: '1', optionText: 'HttpClient' },
+ { optionValue: '2', optionText: 'WebSocket' },
+ { optionValue: '3', optionText: 'NgRx' },
+ { optionValue: '4', optionText: 'JSON' }
+ ],
+ answer: '1',
+ explanation: 'a server makes an HTTP request using the HttpClient service',
+ selectedOption: ''
+ },
+ {
+ questionId: 9,
+ question: 'In which of the following can Angular use services?',
+ options: [
+ { optionValue: '1', optionText: 'Lazy-loaded modules' },
+ { optionValue: '2', optionText: 'Eagerly loaded modules' },
+ { optionValue: '3', optionText: 'Feature modules' },
+ { optionValue: '4', optionText: 'All of the above.' },
+ ],
+ answer: '4',
+ explanation: 'Angular can utilize services with any of these methods',
+ selectedOption: ''
+ },
+ {
+ questionId: 10,
+ question: 'Which of the following is true concerning dependency injection?',
+ options: [
+ { optionValue: '1', optionText: 'It is a software design pattern.' },
+ { optionValue: '2', optionText: 'Injectors form a hierarchy.' },
+ { optionValue: '3', optionText: 'Providers register objects for future injection.' },
+ { optionValue: '4', optionText: 'All of the above.' }
+ ],
+ answer: '4',
+ explanation: 'all of these are correct statements about dependency injection',
+ selectedOption: ''
+ } */
+ ];
+
+ constructor(private route: ActivatedRoute, private router: Router) {
+ this.route.paramMap.subscribe(params => {
+ // get the question ID and store it.
+ this.setQuestionID(+params.get('questionId'));
+ this.question = this.getQuestion;
+ });
+ }
+
+ ngOnInit() {
+ this.question = this.getQuestion;
+ this.totalQuestions = this.allQuestions.length;
+ this.timeLeft = this.timePerQuestion;
+ this.progressValue = 100 * (this.currentQuestion + 1) / this.totalQuestions;
+ this.countDown();
+ }
+
+ displayNextQuestionWithOptions() {
+ this.resetTimer();
+ this.increaseProgressValue();
+
+ this.questionIndex = this.questionID++;
+ document.getElementById('question').innerHTML = this.allQuestions[this.questionIndex].question;
+ document.getElementById('question').style.border = this.blueBorder;
+
+ for (this.optionIndex = 0; this.optionIndex < 4; this.optionIndex++) {
+ document.getElementsByTagName('li')[this.optionIndex].innerHTML =
+ this.allQuestions[this.questionIndex].options[this.optionIndex].optionText; // add option text for list items
+ }
+ }
+
+ displayPreviousQuestion() {
+ this.resetTimer();
+ this.decreaseProgressValue();
+
+ this.questionIndex = this.currentQuestion -= 1; // decrease the question index by 2 for previous question
+ document.getElementById('question').innerHTML = this.allQuestions[this.questionIndex].question;
+ document.getElementById('question').style.border = this.blueBorder;
+ }
+
+ navigateToNextQuestion(): void {
+ this.currentQuestion++;
+
+ if (this.isThereAnotherQuestion()) {
+ this.router.navigate(['/question', this.getQuestionID() + 1]); // navigates to the next question
+ this.displayNextQuestionWithOptions(); // displays the next question
+ }
+
+ this.resetTimer();
+ }
+
+ navigateToPreviousQuestion(): void {
+ this.currentQuestion--;
+ this.router.navigate(['/question', this.getQuestionID() - 1]); // navigates to the previous question
+ this.displayPreviousQuestion(); // display the previous question
+ }
+
+ // increase the correct answer count when the correct answer is selected
+ incrementCorrectAnswersCount() {
+ if (this.question && this.question.selectedOption === this.question.answer) {
+ this.correctAnswersCount++;
+ this.correctAnswer = true;
+ } else {
+ this.correctAnswer = false;
+ }
+ }
+
+ // checks whether the question is a valid question and is answered correctly
+ checkIfValidAndCorrect(): void {
+ if (this.question && this.currentQuestion <= this.totalQuestions &&
+ this.question.selectedOption === this.question.answer) {
+ this.incrementCorrectAnswersCount();
+ this.disabled = false;
+ this.elapsedTime = Math.floor(this.timePerQuestion - this.timeLeft);
+ this.elapsedTimes.push(this.elapsedTime);
+ this.quizDelay(3000);
+ this.navigateToNextQuestion();
+ }
+ }
+
+ // increase the progress value when the user presses the next button
+ increaseProgressValue() {
+ this.progressValue = 100 * (this.currentQuestion + 1) / this.totalQuestions;
+ }
+
+ // decrease the progress value when the user presses the previous button
+ decreaseProgressValue() {
+ this.progressValue = (100 / this.totalQuestions) * (this.getQuestionID() - 1);
+ }
+
+ // determine the percentage from amount of correct answers given and the total number of questions
+ calculatePercentage() {
+ this.percentage = 100 * (this.correctAnswersCount + 1) / this.totalQuestions;
+ }
+
+ recordSelections() {
+ if (this.question.selectedOption !== '') {
+ this.totalSelections++;
+ }
+ }
+
+ /**************** public API ***************/
+ getQuestionID() {
+ return this.questionID;
+ }
+
+ setQuestionID(id: number) {
+ return this.questionID = id;
+ }
+
+ isThereAnotherQuestion(): boolean {
+ return this.questionID <= this.allQuestions.length;
+ }
+
+ get getQuestion(): QuizQuestion {
+ return this.allQuestions.filter(
+ question => question.questionId === this.questionID
+ )[0];
+ }
+
+ // countdown timer and associated methods
+ private countDown() {
+ this.interval = setInterval(() => {
+ if (this.timeLeft > 0) {
+ this.timeLeft--;
+ this.recordSelections();
+
+ // utilized for disabling the next button until an option has been selected
+ if (this.question.selectedOption === '') {
+ this.disabled = true;
+ } else {
+ this.disabled = false;
+ }
+
+ if (this.question && this.currentQuestion <= this.totalQuestions && this.question.selectedOption !== null) {
+ this.totalQuestionsAttempted++;
+ }
+
+ this.checkIfValidAndCorrect();
+ this.calculatePercentage();
+ this.calculateTotalElapsedTime(this.elapsedTimes);
+
+ // check if the timer is expired
+ if (this.timeLeft === 0 && this.question && this.currentQuestion <= this.totalQuestions) {
+ this.question.questionId++;
+ this.displayNextQuestionWithOptions();
+ this.resetTimer();
+ }
+
+ if (this.question.questionId > this.totalQuestions) {
+ this.router.navigateByUrl('/results'); // todo: pass the data to results!
+ }
+ }
+ }, 1000);
+ }
+
+ private resetTimer() {
+ this.timeLeft = this.timePerQuestion;
+ }
+ private stopTimer() {
+ this.timeLeft = 0;
+ }
+
+ private calculateTotalElapsedTime(elapsedTimes) {
+ this.completionTime = elapsedTimes.reduce((acc, cur) => acc + cur, 0);
+ }
+
+ quizDelay(milliseconds) {
+ const start = new Date().getTime();
+ let counter = 0;
+ let end = 0;
+
+ while (counter < milliseconds) {
+ end = new Date().getTime();
+ counter = end - start;
+ }
+ }
+}
diff --git a/quiz/src/app/containers/results/results.component.html b/quiz/src/app/containers/results/results.component.html
new file mode 100644
index 000000000..b3fe2f6af
--- /dev/null
+++ b/quiz/src/app/containers/results/results.component.html
@@ -0,0 +1,89 @@
+
+
+
+ Dependency Injection Quiz
+
Results
+
+
+
+
+
+
Statistics
+ You answered {{ correctAnswersCount }} out of {{ totalQuestions }} questions correctly.
+ You completed the quiz in {{ elapsedMinutes }} minutes and {{ elapsedSeconds }} seconds.
+
+