Pomodoro-App mit Angular selber programmieren

So baust du dir deine eigene Pomodoro-App

Um produktiv zu sein, ernähren sich viele meiner IT-Artgenossen zu großen Teilen von Kaffee. Dabei gibt es doch noch andere Möglichkeiten, etwas fertig zu bekommen. Keine Macht den Drogen! Wir bleiben trotzdem in der Küche und zeigen, wie man mit ein bisschen Code seinen eigenen Küchen-Timer für gesteigerte Produktivität baut. Vorraussetzungen für diese Anleitung sind solide HTML und JS-Kenntnisse, am Besten in Form von Jade und CoffeeScript.

Die ‚Pomodoro-Technik‘ wurde in den späten 80ern von Francesco Cirillo zum Zweck des Zeitmanagements entwickelt. Dabei nutzte er eine tomatenförmige Küchenuhr, um seine Arbeitszeit in 25-minütige Arbeitsphasen gefolgt von fünfminütigen Pause zu teilen. Eben jene Pausen sollen Produktivitätssteigernd wirken. In genau diesem Moment schreibe ich unter dem Einfluss dieser leistungssteigernden Tomate, und es fühlt sich einfach wunderbar an.

Leider konnte ich keinen Timer, außer der OSX-App Pomodoro One finden, der genau das tut was ich will: Mich nach 25 Minuten über den Beginn der Pause in Kenntnis setzen und den Timer für die Pause starten. Doch hier verzage ich nicht. Ein echter Tomatensaftblut-Hacker nimmt hier sein Schicksal in die Hand und baut seinen eigenen Pomodoro-Timer. In diesem Artikel zerlegen wir den Timer in seine Einzelteile.

Timer-App für den Browser programmieren

Pomodoro-App auf CodePen zusammenbasteln

Ziel ist ein Timer, der zwei unterschiedlich lange Zeitspannen abwechselnd ablaufen lässt und dem Nutzer Bescheid gibt, sobald eine Phase endet. Für derartig kleine, auf den Browser beschränkte Applikationen ist die Plattform meiner Wahl der Online-Code-Sandkasten CodePen.

Wir setzen hier auf das JavaScript-MVVM-Framework AngularJS, welches von Google entwickelt wird. Damit die Entwicklung nicht durch allzu viel Redundanz in der Syntax des Qullcodes ausgebremst wird, nutze ich Jade, Stylus und Coffeescript als Template- bzw. Präprozessorsprachen für HTML, CSS und JavaScript. Die HMTL, CSS und JS-Version des Quellcodes zu sehen, kann mit dem kleinen, oberhalb der Editorfelder liegenden ‚View Compiled‘-Button angezeigt werden. Wir betrachten in diesem Artikel nicht das Styling der App, sondern rein funktionale Aspekte, sprich Struktur und Programmierung.

Als Erstes tauchen wir in die HTML-Struktur der Applikation ein:

doctype html
html(ng-app="pomodoroApp")
head
link(href='http://fonts.googleapis.com/css?family=Open+Sans:400,300' rel='stylesheet' type='text/css')
title Pomodorski
.card(ng-class="{work: !break && running, break: break && running, booted: booted}" ng-controller='PomodoroController')
.countdown {{countDown | clockTime}}
.controls
input(ng-model='workDuration' )
input(ng-model='breakDuration')
button(ng-click="startSession('work')") Work
button(ng-click="startSession('break')") Break

Das Attribut ng-app="pomodoroApp" ist eine so genannte Direktive. Direktiven dienen dazu, Bereiche oder Elemente in der Applikation zu markieren, die von AngularJS verarbeitet und erweitert werden. In diesem Fall handelt es sich um das „Territorium“ des Applikationsmoduls pomodoroApp. Der Timer im Kartenformat, hier mit der Klasse .card versehen, ist der Zugriffsbereich des Controllers, also dessen View. AngularJS ist ein sogenanntes Model-View-ViewModel Framework. Das folgende Diagramm stellt die Bereiche innerhalb einer Angular-Applikation dar:

Die gerade erwähnte View sieht und hat Zugriff auf die innerhalb des ViewModels liegenden Variablen. Diese Variablen werden mit Werten aus dem Model populiert. Somit hat weder das Model noch die View Kenntnis von der jeweils anderen Komponente, ihre einzige Schnittstelle ist das so genannte ViewModel. In AngularJS stellt der $scope Dienst das ViewModel zur Verfügung. In unserem Fall modifiziert der PomodoroController unser ViewModel und die View reagiert auf jede dieser Änderungen und zeigt sie an. Diese Verhaltensweise wird als Reaktivität bezeichnet. Betrachten wir die View nun genauer.

.card(ng-class="{work: !break && running, break: break && running, booted: booted}" ng-controller='PomodoroController')
  .countdown {{countDown | clockTime}}
  .controls
    input(ng-model='workDuration' )
    input(ng-model='breakDuration')
    button(ng-click="startSession('work')") Work
    button(ng-click="startSession('break')") Break

Die ng-class Direktive dient dazu, der Timer-Karte Klassen zuzuweisen (Überraschung!), und erlaubt somit, den aktuellen Zustand des Timers farblich wider zu spiegeln. .countdown {{countDown | clockTime}} zeigt die verbleibende Zeit der aktuellen Phase an. Da der Controller nicht im MM:SS-Format arbeitet, sondern nur Sekunden herunter zählt, wird die Ausgabe mithilfe des clockTime Filters in ein für uns einfacher lesbares Format umgewandelt. Die ng-model Direktiven weisen den Eingabefeldern Variablen im $scope zu. Somit können wir selbst Zeiten für unseren Arbeitsrhythmus definieren. Ein ng-click auf die Buttons startet eine neue Phase, also entweder Arbeitszeit oder Pause.
Im folgenden betrachten wir den Backend-Code.

app = angular.module 'pomodoroApp', []
app.controller 'PomodoroController',
  class PomodoroController
    constructor: ($scope,$interval) ->

Zunächst wird ein neues Angular-Modul initialisiert, welches wir als Applikation einsetzen. Module können jedoch auch als einzelne Teile einer Applikation eingesetzt werden. Somit sind wir in der Lage Applikationen als Teile eines größeren Programms einsetzen. Dem App-Modul wird nun der in der View bereits zugewiesene Controller übergeben. Das Prinzip der Dependency Injection erlaubt uns, Dienste an den Controller zu übergeben. Das ist hier zum Einen $scope und zum Anderen $interval welchen wir später für unseren Countdown nutzen.

# Ask for notifications
Notification.requestPermission (status) =>
  notify('Notifications enabled')
  @notificationsEnabled = status is "granted"
notify = (message)-> new Notification(message, {body: "Pomodorski", icon: "http://www.wpclipart.com/food/fruit/tomato/tomato.png"});

Wir wollen, dass der Nutzer über den Beginn einer Pause bzw. Arbeitsphase informiert wird. Doch weil ein alert() die Ausführung von JS pausiert und somit den Eingriff des Nutzers erfordert, setzen wir auf die Notification API moderner Browser. Zunächst fragen wir die Berechtigungen zum Senden von Notifications an. Die übergebene Callback-Funktion speichert, ob die Erlaubnis gewährt wurde um ggf. den Nutzer später über das fehlen der Notifications in Kenntnis zu setzen. Läuft aller glatt, teilen wir mit, dass Notifications nun verfügbar sind. Die notify nutzen wir, um die Syntax für das Senden neuer Notifications etwas zu vereinfachen.

# setup
$scope.booted = true
$scope.running = false
$scope.break = false
$scope.workDuration = 25
$scope.breakDuration = 5
$scope.countDown = $scope.workDuration * 60
@notificationsEnabled = false

In diesem Abschnitt wurden die Standardwerte der Applikation definiert.

# converter method
# converter method
sessionDurationInSeconds = (type) =>
  if type == 'work'
    duration = $scope.workDuration
    $scope.break = false
  if type == 'break'
    duration = $scope.breakDuration
    $scope.break = true
  duration * 60

Die sessionDurationInSeconds Methode gibt die Dauer der angefragten Session in Sekunden zurück. Der Ausgabewert basiert auf den Werten im Viewmodel.

# countdown method
count = -> $scope.countDown--

Die count Methode reduziert den Wert von countDown, also die momentan verbleibende Zeit in Sekunden, um eins.

$scope.startSession = (type) =>
  alert('Warning: no notifications') if !@notificationsEnabled
  # stop old timer and set session properties
  $interval.cancel @currentSession if angular.isDefined @currentSession
  $scope.countDown = sessionDurationInSeconds(type)
  @currentSession = $interval count , 1000, $scope.countDown
  @currentSession.then () ->
    newSessionType = if type == 'work' then 'break' else 'work'
    $scope.startSession(newSessionType)
    notify('Time for ' + newSessionType)
  $scope.running = true

Falls Notifications nicht verfügbar sein sollten wenn startSession aufgerufen wird, geben wir eine Warnung aus. Falls bereits eine Countdown-schleife existieren sollte, stoppen wir sie. Nun wird eine neue $interval Schleife gestartet. Sie ruft jedes mal unsere count Methode auf, verzögert um 1000 Millisekunden und zwar so oft, wie Sekunden im countdown definiert sind. Mit then wird der Start einer neuen Schleife ausgelöst, sobald die aktuelle endet.

app.filter 'clockTime', ->
  (totalSeconds) ->
      hours = Math.floor(totalSeconds / 3600)
      totalSeconds %= 3600
      minutes = Math.floor(totalSeconds / 60)
      seconds = totalSeconds % 60
      seconds = "0" + seconds if seconds < 10
      minutes + ':' + seconds

Der letzte Teil der Applikation ist der clockTime Filter. Er wandelt eine Eingabe in Sekunden in das für uns lesbare MM:SS Format um.

Fazit Pomodoro-App mit Angular

Times up!

Die Entwicklung einer Applikation diesen Maßstabs stellt einen sehr guten Einstieg in AngularJS dar und bietet ein befriedigendes Erfolgserlebnis. Ich hoffe, dass auch du auf den Geschmack von Tomaten-Timern und Angular gekommen bist.

Vielen Dank fürs Lesen!

Jan Wirth

Studierender Frontend-Enthusiast der Dualen Hochschule Mosbach