So we are finally at the most important component. The first PoC technically worked, but no one would be happy to use the tool as is. So we have to apply the same styles and create a more interactive experience.
Keyboard Events
The key functionality is to type the word you see, letter by letter. For the kids this should be visualized in a way they don’t get lost easily. I decided to not provide a classical input field but just register for keyboard events directly. Altough the application so far is designed to be responsive, I never had in mind to make it usably on the mobile. But on the other hand it would be really, really nice to have it on the go (for instance in the car). Let’s think about this later.
Adding a host listener works quite well to caputre all events.
@HostListener('window:keyup', ['$event']) keyUp(event: KeyboardEvent) { const newWord = (this.userWord + event.key).toUpperCase(); if (this.currentWord.startsWith(newWord)) { this.userWord = newWord; this.checkWord(); } else { this.millisSpent += 2_000; // penalty this.highscoreMissed = this.millisSpent > this.lesson.bestTime; } }
The checkWord function is just checking for the completion of the word and initiates the next word – or in case it was the last word returns to the overview screen.
checkWord() { if (this.userWord === this.currentWord) { this.messageService.add({ key: 'lesson', severity: 'success', summary: 'Super', detail: 'Du hast es drauf!', }); timer(1_000).pipe(first()).subscribe(this.next.bind(this)); } }
The next operation is actually the most complicated part as we need to store the highscore, save the completion status etc.
next(): void { if (this.lesson.words.length > this.index + 1) { this.index++; this.setupWord(this.lesson.words[this.index]); this.messageService.clear(); } else { this.timerSubscription.unsubscribe(); // stop timer this.lessonService .completeLesson(this.lesson, this.millisSpent) .subscribe(() => { this.navigationService.showLessons(); }); } }
And here the complete function. I’ll have to think this through again to make it easier to test. With RxJS you can really mess up a lot. I haven’t used transactions and I’d like to split it up into smaller pieces that can be tested individually. This article explains quite well what we should watch out for.
completeLesson(lesson: Lesson, millisSpent: number): Observable<any> { return this.fireAuth.user.pipe( switchMap((user) => { return this.firestore .collection(`user-data/${user.uid}/lessons`) .doc(lesson.id) .get(); }), switchMap((doc) => { if (!doc.exists || millisSpent < doc.data().bestTime) { return doc.ref .set({ bestTime: millisSpent, }) .then(() => doc); } else { return of(doc); } }), switchMap((doc: DocumentData) => { return doc.ref .collection('results') .add({ timeStamp: new Date(), millisSpent: millisSpent, }) .then(() => null); }) ); }
Timer
As soon as the user types the first key a timer is started. As long as it is your first run or you are below you last highscore it should be displayed green – else it switches to red.
With RxJS we get a powerful tool to implement such features.
@HostListener('window:keyup', ['$event']) keyUp(event: KeyboardEvent) { if (!this.timerSubscription) { this.timerSubscription = timer(1_000, 1_000).subscribe((time) => { this.millisSpent += 1_000; this.highscoreMissed = this.millisSpent > this.lesson.bestTime; }); }
…and cleanup
ngOnDestroy() { if (this.timerSubscription) { this.timerSubscription.unsubscribe(); } }
The rendering is simple using the built in Angular pipe.
<!-- Timer --> <div *ngIf="timerSubscription" class="lesson__timer" [ngClass]="{ 'lesson__timer--highscore_missed': highscoreMissed }" > <h2>{{ millisSpent | date: "mm:ss" }}</h2> </div>
I don’t really like to check for the timerSubscription, but why add an extra attribute? Maybe subject to refactoring. But other than that I think it should work.
What next
Now we are coming to the fun part soon: the tuning and refactoring. I really want to explore how to further improve the application in terms of code quality, performance and usability.
But first: we need a simple way to upload lessons. For this I’d like to use the admin SDK and a super simple way to just upload images and lessons.