Typolino – PoC

Now let’s finalize the PoC! There are so many approaches to write software. What worked best for me so far is to start with the basics and make sure everything works. Name it evolutionary prototyping with the main focus on integrating most of the stuff horizontally.

PrimeNg

I’ don’t want to focus on styling the application right now. Nonetheless I added PrimeNG dependencies and styles. For me this is one of the best -if not the best set of widgets available for Angular.

 
npm install primeng --save
npm install primeicons --save

Register the styles in angular.json

Lessons Component

The only job of this component is to load the data and provide the data for the template. In addition it has an operation to select a single lesson and navigate to the actual lesson.

import { Component, OnInit } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore/firestore';
import { LessonService } from '../lesson.service';
import { Observable } from 'rxjs';
import { Lesson } from '../lesson';
import { Router } from '@angular/router';

@Component({
  selector: 'app-lessons',
  templateUrl: './lessons.component.html',
  styleUrls: ['./lessons.component.less']
})
export class LessonsComponent implements OnInit {
  lessons$: Observable<Lesson[]>;

  constructor(private lessonService: LessonService, private router: Router) { }

  ngOnInit(): void {
    this.lessons$ = this.lessonService.selectLessons();
  }

  selectLesson(lesson: Lesson) {
    this.router.navigate(['/lesson', lesson.id]);
  }

}

The template is super simple as well. I haven’t styled it yet but I already structured it a bit. As I intend to render the lessons as cards, I already use them here. This is good enough to verify the stuff works.

<h2>Select a lesson</h2>

<ng-container *ngIf="lessons$ | async; let lessons">
    <p-card *ngFor="let lesson of lessons " styleClass="lessons__card">
        <p-header>
            {{lesson.name}}
        </p-header>
        {{lesson.description}}
        <p-footer>
            <button (click)="selectLesson(lesson)">select</button>
        </p-footer>
    </p-card>
</ng-container>

Lesson Component

The lesson component is a bit more complicated. It should render one word at a time and allow to enter some data, verify it and move on to the next word. This component may undergo some major refactorings, but here is how it looks like for the PoC:

import { Component, OnInit, Input } from '@angular/core';
import { LessonService } from '../lesson.service';
import { ActivatedRoute, Router } from '@angular/router';
import { map, switchMap } from 'rxjs/operators';
import { combineLatest, Observable } from 'rxjs';
import { Lesson } from '../lesson';
import { Word } from '../word';

@Component({
  selector: 'app-lesson',
  templateUrl: './lesson.component.html',
  styleUrls: ['./lesson.component.less'],
})
export class LessonComponent implements OnInit {
  lesson: Lesson;
  currentWord: string;
  imageUrl$: Observable<string>;
  audioUrl$: Observable<string>;
  index = 0;
  points = 0;
  nextDisabled: boolean;
  userWord: string;

  constructor(
    private lessonService: LessonService,
    private route: ActivatedRoute,
    private router: Router
  ) { }

  ngOnInit(): void {
    this.route.paramMap
      .pipe(
        map((params) => params.get('lessonId')),
        switchMap((lessonId: string) =>
          this.lessonService.selectLesson(lessonId)
        )
      )
      .subscribe((lesson) => {
        this.lesson = lesson;
        this.setupWord(this.lesson.word[0]); // just pass the first word.
      });
  }

  setupWord(word: Word) {
    this.nextDisabled = true;
    this.imageUrl$ = this.lessonService.getImageFileUrl(this.lesson, word);
    this.audioUrl$ = this.lessonService.getAudioFileUrl(this.lesson, word);
    this.currentWord = word.word;
    this.userWord = '';
  }

  checkWord() {
    // send success message / animation
    
    if (this.userWord === this.currentWord) {
      this.nextDisabled = false;
    }
  }

  next(): void {
    if (this.lesson.word.length > this.index + 1) {
      this.index++;
      this.setupWord(this.lesson.word[this.index]);
    }
  }
}

…and the template..

<h2>{{lesson.name}}
</h2>

<div>
    {{lesson.word[index].word}}
</div>
<div *ngIf="imageUrl$ | async; let imageUrl">
    <img [src]="imageUrl">
</div>
<div>
    {{lesson.word[index].sentence}}
</div>

<div>
    <input type="text" [(ngModel)]="userWord" (ngModelChange)="checkWord()">
</div>

<div>
    <button (click)="next()" [disabled]="nextDisabled">next</button>
</div>

Servive

And to complete the picture, here is the complete service. Note the two services to return the URL for the images and audio files.

import { Injectable } from '@angular/core';
import { Lesson } from './lesson';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { AngularFirestore } from '@angular/fire/firestore';
import { AngularFireStorage } from '@angular/fire/storage';
import { Word } from './word';

@Injectable({
  providedIn: 'root',
})
export class LessonService {
  constructor(
    private firestore: AngularFirestore,
    private filestorage: AngularFireStorage
  ) { }

  selectLessons(): Observable<Lesson[]> {
    return this.firestore
      .collection<Lesson>('lessons')
      .valueChanges({ idField: 'id' });
  }

  selectLesson(lesosnId: string): Observable<Lesson> {
    return this.firestore
      .collection('lessons')
      .doc<Lesson>(lesosnId)
      .valueChanges()
      .pipe(
        map((lesson) => { // todo: unhappy, any better way?
          lesson.id = lesosnId;
          return lesson;
        })
      );
  }

  getAudioFileUrl(lesson: Lesson, word: Word): Observable<string> {
    return this.filestorage
      .ref(`${lesson.id}/${word.audioId}`)
      .getDownloadURL();
  }

  getImageFileUrl(lesson: Lesson, word: Word): Observable<string> {
    return this.filestorage
      .ref(`${lesson.id}/${word.imageId}`)
      .getDownloadURL();
  }
}

Result

The result looks crappy, but I’m still impressed with how little code we can create an application with Angular and Firebase. Next steps would be to add authentication in order to track the progress / score per user. Then I think we have our main functionality complete and we can focus on styling and UX.

My kids already tried it out and had (some) fun. But of course it needs improvements to keep them motivated and keep frustration away from them as much as possible. Early testing is generally a good thing if you have a chance to do it. I haven’t thought about this from the first moment, but I realize now that I will completely neglect case sensitivity.

The lesson selection
training mode

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.