Wireframing van een SPA

Een SPA bestaat vaak uit een login-gedeelte en een applicatie gedeelte. Beiden kunnen via wireframing (een simpel ontwerpproces), in overleg met de opdrachtgever worden gemaakt. De ontwerper maakt een wireframe, en toont dat aan de opdrachtgever. Nadat ze, na eventuele wijzigingen, het eens zijn geworden kan de applicatie worden gebouwd. Hieronder is een voorbeeld van een wireframing proces.

Wireframing

Het login gedeelte
Dan de applicatie-pagina, dat is een terugkerende hoofdindeling. Bovenin is een Featured Area, dit is over de artiest die actief naar geluisterd wordt, rechts het audio-gedeelte over het album van deze artiest waarnaar wordt geluisterd, en links onderin de overige applicatie onderdelen.
Laten we beginnen met de Featured Area, daarin komt een titel met de tekst: "Featured", daaronder een afbeelding van de artiest, rechts daarvan de naam van de artiest, en daaronder een tekstblok dat iets over de artiest zegt.
De volgende stap is de applicatie-functionaliteit in de tabbladen.
We maken vier tabbladen: My Music, My Account, en nog twee, later in te vullen.
My Music zal alle albums die ik mag beluisteren tonen. De lijst kan lang zijn, dus komt er een scrollbar rechts. We zetten er een subtitle in om de sectie kenbaar te maken.
Daaronder komen placeholders voor de albums, afbeeldingen met daaronder in vet, de albumtitel en in gewoon de naam van de artiest. We dupliceren dit een aantal malen, horizontaal en verticaal. De onderste rij kappen we af om de illusie van scrollen duidelijk te maken.
My Account bevat gegevens over het account, mailadres, wachtwoord, soort van abonnement, etc.
Dan aan de rechterkant, zoals aangegeven, het panel dat aangeeft wat er nu speelt. Op dat panel komen audio-speler-controls, en geluid-controls. We zetten een album-placeholder erboven, en een tracklist eronder.
Het My Account wireframe, dat onder een ander tabblad is gekomen.
Dan zijn er nog twee tabbladen, die moeten nog ontworpen worden. Ik wil dat graag voor u doen.
Of een andere applicatie voor u ontwerpen.

Een “Single Page Application”

Bijna iedereen die veel websites bezoekt heeft een SPA (single page application) meegemaakt. Gmail, Google Maps, Netflix, Twitter…. een lange lijst. Het zijn allemaal SPA’s. Als je door Gmail heen navigeert, dan merk je dat er niet zoveel verandert. De “sidebar”, de “header” blijft hetzelfde als je van “Inbox” naar “Verzonden” gaat. De lijst met emails verandert, maar de layout en onderdelen van de pagina blijven hetzelfde. Dus de pagina wordt niet opnieuw geladen, alleen de lijst van emails verandert, alleen de delen die de inhoud wijzigen veranderen. Dit is het basisprincipe van een SPA. Dit zorgt ervoor dat het laden van die onderdelen veel sneller gaat, de pagina wordt niet opnieuw geladen, en zijn geen grote gebeurtenissen binnen de browser van de webbezoeker.

De ervaring voor de gebruiker is rustiger, de applicatie vertoont geen grote veranderingen, geen witte lege pagina’s, ook niet een fractie van een seconde. En de server heeft het ook makkelijker, die hoeft alleen maar dat stukje te doen wat verandert. Een win-win situatie.

Wat is er zo speciaal aan een SPA en hoe is dit mogelijk?

Op de meeste websites zijn op iedere pagina delen die nooit veranderen. Dit is een keuze. Een web-applicatie hoort op alle pagina’s dezelfde layout te hebben zodat de gebruiker makkelijk zijn weg kan vinden. We denken dan aan “headers”, “footers”, logo’s, “navigation-bar”, soms veranderen deze dingen wel binnen een sectie van een applicatie, maar zijn dan binnen die sectie toch weer consistent. SPA’s maken van deze ergonomische conditie een voordeel. De laadtijd van een applicatie per onderdeel is sneller, en het beeld is rustiger. Indien het asynchroon (reactive) is geprogrammeerd (iets dat binnen Angular de standaard is), dan hoeft de gebruiker zelfs niet te wachten tot dat de nieuwe items compleet zijn verzameld maar kan de pagina worden geupdate terwijl de items binnen komen. De applicatie blijft responsive. Omdat reactive ook betekent dat het laden geannuleerd kan worden kan de gebruiker ervoor kiezen om het laden halverwege af te breken door een andere keuze te maken. Bijvoorbeeld als de webapplicatie bezig is om de afbeelding van een televisie te laden kan de gebruiker beseffen dat hij verkeerd heeft geklikt, en alsnog op het goede klikken waardoor het laden stopt en de gebruiker direct naar een ander item gaat.

Qua ervaring werkt het zoals weergegeven op deze afbeelding (hierboven). In de linkse situatie moet de gebruiker wachten totdat de pagina klaar is met vernieuwen, en is het web, zeker bij een slechte netwerkverbinding enkele seconden niet responsive. Op mobiele apparaten, waar de netwerkverbinding soms een paar seconden wegvalt is dit al helemaal desastreus.

In een SPA applicatie is dit niet, en kan de gebruiker altijd het laden onderbreken, en is de hoeveelheid data die geladen moet worden beperkt, blijft het overgrote deel van de pagina, dat deel dat niet wordt gewijzigd, gewoon op het scherm. Geen flitsen, geen laadtijd, en al die tijd responsive. Dit is erg belangrijk voor een goede gebruikerservaring en in modern web-design onontbeerlijk.

Waarom houden ontwikkelaars van SPA?

Er zijn bezwaren. De leercurve is nogal steil. Verschillende concepten moeten tegelijkertijd worden geleerd en toegepast. Het reactive programmeren vereist een andere manier van nadenken, het observer-patroon moet worden begrepen. Maar als deze horde is genomen, en er is geoefend, dan komen er snel mooie resultaten. Immers het html-gedeelte blijft gelijk. Het is zo dat het vanwege de modulaire opzet eenvoudiger wordt. Men bouwt geen hele pagina’s meer, maar slechts gedeelten van een pagina, en de interactie tussen de delen, daarvoor zorgt de SPA-ontwikkel omgeving (bijvoorbeeld Angular).

Een ander voordeel van SPA is het loskoppelen van de back-end met de front-end, dit in tegenstelling tot de technieken waarin deze juist enorm zijn verweven. Door de loskoppeling is het mogelijk om meerdere interfaces te maken op dezelfde back-end bron. Data worden opgevraagd via REST-API’s op het moment dat het er toe doet. Ze worden niet meer ongevraagd meegeleverd. Ook is het eenvoudig mogelijk om meerdere servers op een web-applicatie te integreren. In een vierkantje op een sportpagina kan bijvoorbeeld een weer-applicatie worden getoond, of de kwaliteit van het zwemwater. Integratie van services kan op de client plaatsvinden. Plugin’s kunnen de bloeddruk tonen van een patiënt binnen de pagina waarin de patient’s NAW gegevens staan, en als daarna de bloedsuiker wordt gemeten kan die ook als plugin worden getoond. Door inzet van CORS kan een aanval via cross-site scripting worden voorkomen.

Voor programmeurs en web-designers is het eenvoudig om een multi-functionele web-applicatie te schrijven en daarin gebruik te maken van vele services.

Single Page vs Multi Page?  (SPA vs MPA)

Een lijstje van voor en tegen, zonder de illusie compleet te zijn:

SPAMPA
voortegenvoortegen
SPA is snel, de meeste resources worden maar een keer geladen. Alleen data worden heen en weer gezonden.De frameworks zelf worden in de client geladen waardoor SPA's langzamer laden. Dit wordt wel weer gecompenseerd door slimme (application-level) caching, en doordat de frameworks met reactive programming vanaf het begin responsive zijn.MPA's geven een heldere structuur van een applicatie. Niet functionaliteit wordt gedistribueerd, maar pagina's vooraf functioneel ingericht.Front-end en back-end zijn vast gekoppeld. De front-end wordt op de server opgebouwd.
Het ontwikkelen is modulair en gestroomlijnd. Er worden geen pagina's op de server opgebouwd, de serverbelasting is veel kleiner.Javascript moet actief zijn op de browser. Hoewel, indien dit niet zo is, bijvoorbeeld in sommige HMI's zijn er mogelijkheden om de pagina's server-side te renderen.Betere SEO is mogelijk omdat pagina's keywords per pagina kunnen inrichten waardoor ze een hogere ranking krijgen wanneer deze keyword gebaseerd is.Ontwikkelen kan complexer zijn doordat frameworks voor de server alsook voor de client moeten worden geschreven. Applicatiebouw duurt hierdoor langer.
SPA is eenvoudig te debuggen binnen Chrome waarin ontwikkeltools zijn geïntegreerd.
SPA's zijn makkelijk te transformeren naar andere devices, omdat dezelfde achtergrond-code wordt gebruikt.
SPA's kunnen zelfs tijdelijk zonder netwerkverbinding blijven werken, zonder dat de gebruiker hoeft te merken dat het netwerk instabiel is.

SPA? Angular, React, Vue?

Dit zijn ontwikkelomgevingen die het op efficiënte en eloquente wijze mogelijk maken om SPA’s te schrijven. Deze frameworks maken gebruik van herbruikbare componenten die in community-circuits te verkrijgen zijn. Niet iedere keer moet het wiel opnieuw worden uitgevonden. Het bouwen van web’s wordt veel sneller. Welke omgeving (Angular, React, Vue) de voorkeur geniet? Er zijn veel argumenten voor of tegen iedere omgeving. Immers is het ook belangrijk waarin je goed bent. Veranderen van programmeerconcept kost tijd, het veroorzaakt stress. Maar soms kan het niet anders.

Ik wil wel een paar punten meegeven ter overweging:

  • Hoe volwassen (mature) zijn de frameworks, libraries?
  • Worden de frameworks komende tijd nog wel voldoende ondersteund (is er een grote stabiele gebruikers-basis, zijn ze open source?)
  • Is er veel steun vanuit de communities bij het oplossen van problemen (kijk eens op stackoverflow)
  • Hoe makkelijk is het om goede programmeurs voor een framework te vinden?
  • Hoe schaalbaar (in complexiteit) zijn de frameworks, kun je makkelijk kleine applicaties schrijven, kun je makkelijk grote applicaties schrijven?
  • Hoe is de leer-curve voor een framework?
  • Welke performance verwacht je van een framework?

Migratie van MPA naar SPA?

Nu, de leercurve, die is wel lastig. Het vereist vele andere manieren van denken. De architectuur is anders, modulair denken, het reactive programmeren, het gebruiken van het Observer-patroon. Ook organisatorisch kan het consequenties hebben. Was er voorheen een taak voor een webdevelopers-team, het schrijven van een back-end en het schrijven van een front-end, en deze vast aan elkaar koppelen. Het front-end werd immers door het back-end gegenereerd. In De SPA situatie is dit volledig anders. De back-end programmeurs hoeven niet meer te weten wat de front-ends doen, op welke apparaten ze worden getoond. De front-end programmeurs hoeven niets over de backend te weten. Welke databases er zijn. Ze hoeven alleen maar de REST-API’s te kennen, en hoe de data worden opgeslagen/verwerkt is niet hun probleem.

Niet alleen de web-applicaties worden modulair, maar ook de bedrijfsorganisatie wordt modulair. Dit is een voordeel indien bedrijven in staat zijn om zonder kleerscheuren deze migratie uit te voeren. Immers de rollen die mensen vanuit oudsher hebben gaan wijzigen, en soms kan dit ook pijnlijk zijn. Dit zijn geen processen van een nacht ijs en moeten goed doordacht worden, willen we voorkomen dat er mensen gefrustreerd rondlopen.

Echter er zijn ook argumenten die het minder erg maken dan het op eerste gezicht lijkt. De backend wordt simpeler, maar de databases blijven hetzelfde. De bouwers van de front-end kunnen gebruik maken van de html-code van het MPA netwerk dat gaat verdwijnen. Ook grafische elementen kunnen worden hergebruikt. De kosten van de migratie kunnen dus meevallen, en de kosten van toekomstige applicaties kunnen per functie punt fors lager uitvallen.

Uiteindelijk zal de migratie op een scherpe markt wel moeten voor een bedrijf dat van internetgebruik en complexe applicaties afhankelijk is. Zeker als men in ogenschouw neemt de enorme en toenemende variëteit van apparaten, en omstandigheden waarmee applicatie-gebruikers de bedrijfsapplicatie willen benaderen. Een salesmanager wil thuis op zijn notebook werken, in de trein op een tablet en in een meeting even gauw op zijn mobiel. Op vliegvelden of op stations zijn er area’s waar het netwerk minder goed is. Het moet allemaal kunnen, zonder hoge ontwikkelkosten, zonder verlies van functionaliteit, door slim programmeren, door frameworks die veel van deze problemen out of the box oplossen. Uiteindelijk zal ieder bedrijf van MPA’s naar SPA’s migreren. De vragen gaan over wanneer, en wat zijn de consequenties.

Reactive Pattern tot RxJS: Observable Pattern

In dit artikel leg ik uit hoe het Observable Pattern kan worden geïmplementeerd. Links is het diagram van het Observable Pattern. Er zijn een paar problemen die bij dit pattern spelen, en die in dit artikel worden opgelost, overigens geheel volgens standaard Angular/RxJS wijze.

Het belangrijkste probleem is dat Subject op dezelfde interface de mogelijkheid biedt om data te observeren, maar tegelijkertijd diezelfde data te zenden. Overal binnen de applicatie ontstaat hierdoor directe toegang tot het Subject. Om dit probleem op te heffen willen we graag een onderscheid maken tussen de mogelijkheid om je aan te melden (subscribe) en de mogelijkheid om te observeren (observe).

Laten we om te beginnen eerst een en ander hernoemen zodat het overeen gaat komen met RxJS, waar we uiteindelijk zullen uitkomen.

We hebben de Observer interface, die heeft in RxJS enkele methoden meer (error-afhandeling etc), maar daar kom ik later op terug. De belangrijkste nu is “next”

export interface Observer {
    next(data:any);
}

We hebben de Subject interface, die heeft ook een “next” functie. De Subject is een uitbreiding op Observer en op Observable, deze relatie geven we expliciet aan.

interface Subject extends Observer, Observable{
}

En we hebben een geëxporteerde interface Observable

export interface Observable {
  subscribe(obs:Observer);
  unsubscribe(obs:Observer);
}

We gaan nu de applicatie bouwen rondom deze drie interfaces. Het doel dient te worden dat we beter kunnen uitschalen in complexiteit, we zullen timing-problemen voorkomen (volgorde van creëren van objecten). Ik herinner graag aan de uitspraak van Venkat Subramaniam: “Do not walk away from Complexity, Run!!!”

We beginnen met een implementatie van Subject, want tot nu toe hebben we alleen maar interfaces. De Subject is i feite een event-bus, en dat is hoe we deze implementeren. Maar we houden Subject private in de applicatie. Dit is heel belangrijk in de Observable pattern.

import * as _ from 'lodash';
class SubjectImplementation implements Subject {

  private observers: Observer[] = [];

  next(data: any) {
    this.observers.forEach(obs => obs.next(data));
  }

  subscribe(obs: Observer) {
    this.observers.push(obs);
  }

  unsubscribe(obs: Observer) {
    _.remove(this.observers, el => el === obs);
  }
}

We hebben een array waarin de Observers worden opgeslagen, we hebben een subscribe, en unsubscribe, ze zullen voor zichzelf spreken, (de “remove” functie komt uit de “lodash” library, zie import-statement).

We maken van de data een Observable, voor dit moment een variabele. De dollar achter de naam geeft aan dat het een stream betreft. Andere delen van de applicatie kunnen nu “subscribe” op deze data.

export let booksList$: Observable;

 

Laten we nu kijken hoe Componenten hier mee om moeten gaan. Het component moet de Observer interface implementeren, en daarom ook de “next” methode implementeren. Op deze manier zullen meerdere Componenten op de data subscriben als Observer. Hieronder een component dat het aantal items op de lijst bijhoudt. We zien nu al voordelen van deze code. Het maakt niet meer uit op welke volgorde de Componenten onderling worden gecreëerd, omdat ze niet langer de eigenaar zijn van de data, maar deze observeren.

export class BooksCounterComponent implements Observer, OnInit {

    booksCounter = 0;

    ngOninit() {
        booksList$.subscribe(this);
    }

    next(data: Book[]) {
        console.log('counter component received data ..');
        this.booksCounter = data.length;
    }
}

Een volgend probleem komt op. We hebben ook een Lijst-component die een array van de data houdt. Dit component kan ook data verwijderen. We moeten ervoor zorgen dat deze Lijst dat niet zelf gaat doen (en dan owner wordt van data of van een kopie ervan), maar dat dit wordt uitbesteed aan de centrale data-owner, de Observable booksList$. We doen dit (tijdelijk) op volgende wijze, we voegen een array toe om data in op te slaan

export let booksList$: Observable;
let books : Book[] = [];

export function initializeBooksList(newList: Book[]){
  books = _.cloneDeep(newList);
}

Dit zal one initiële lijst worden. Deze array is private voor dit bestand. We bieden de gelegenheid om de boekenlijst voor de eerste keer te initialiseren. Om te voorkomen dat ergens een component door gebruik van deze functie de owner wordt van de lijst, gebruiken we cloneDeep van lodash. Ergens kan er nu een Component of service bestaan die de lijst bij opstart initialiseert, zodat de interne books-array is geïnitialiseerd.

We hebben nu componenten en services die niet weten waarvan de data komen. Komen ze uit een back-end, uit een netwerk, of gewoon een lokale source? Componenten weten het niet, en dit maakt dat de data losgekoppeld zijn van de applicatie, en dus wordt de applicatie, bijvoorbeeld, makkelijk om te testen.

Echter, er is nog een probleem. De booksList$ is niet geinitialiseerd, dus die kan nog niet zomaar worden aangeroepen. We gaan dat oplossen. We creeren een SubjectImplementation-object en gaan dat gebruiken binnen de booksList$. We krijgen nu dit:

const booksListSubject = new SubjectImplementation();

export let booksList$: Observable = {
  subscribe: obs => { 
     booksListSubject.subscribe(obs);
     obs.next();
  }
  unsubscribe: obs => booksListSubject.unsubscribe(obs)
}

let books : Book[];

export function initializeBooksList(newList: Book[]){
  books = _.cloneDeep(newList);
  booksListSubject.next(books);
}

Zo, we hebben nu alles met elkaar verbonden. De book-data zijn leeg, maar die kunenn door een externe routine met data gevuld worden. Alle componenten kunnen op de lijst inschrijven, en beschikken dan over de data zonder eigenaar van deze data te worden. Maar er is nog een probleem. Alleen componenten die zijn ingetekend krijgen data-updates, en omdat ze dat een voor een doen, tonen diegenen die nog niet zijn ingetekend geen data. Er is dus nog steeds een timing-probleem, de ene componentn wordt voor de andere gecreeerd en ge”subscribed”. Dit probleem is gekoppeld aan het soort Subject wordt gebruikt.

We gaan een nieuw pattern gebruiken, dat eigenlijk al in ontwikkeling is.

Zie regel 5 en 6, in plaats van alleen te subscriben naar de booksListSubject, we gaan gelijk (op regel 6) een callback naar de server doen om de value die we hebben door te geven. Dus “vroege” subscribers ontvangen een lege array, late subscribers ontvangen de gevulde array.

Dus hoever zijn we nu?

  • Meerdere delen van het programma kunnen intekenen op de data, en krijgen notificaties als er data inkomen. We hoeven niet meer na te denken over de volgorde van creëren.
  • Data zijn niet meer in eigendom van de componenten, de componenten observeren de data en updaten zichzelf als de data updaten.
  • Data kunnen niet meer direct worden gemanipuleerd door de componenten.

We gaan over naar een nieuw pattern, het is een bekend pattern. We hebben nu te maken met data en functies die nauw met elkaar zijn verbonden. We kunnen ze net zo goed in een gezamenlijke class zetten. Dat geeft dan goed weer wat we hier doen.

We gaan een nieuwe gecentraliseerde service inrichten, en we noemen deze service “store”.

Laat ons een kijken wat er in de store moet.

class DataStore {
  private books : Book[];
  private booksListSubject = new SubjectImplementation();

  public booksList$: Observable = {
    subscribe: obs => {
      this.booksListSubject.subscribe(obs),
        obs.next(this.books);
    },
    unsubscribe: obs => this.booksListSubject.unsubscribe(obs)
  }

  initializeBooksList(newList: Book[]){
    this.books = _.cloneDeep(newList);
  }
}

We hebben in de store, volgende zaken:

  • De initializer-functie, die houden we public.
  • De data, in de vorm van de books-array
  • De subject-implementatie, die is ook private.
  • De bookList$ Observable, die is public. Hoewel in typescript public default is, mag het ook expliciet worden aangegeven.

En dan creeren we een constant die de DataStore initialiseert en bekend maakt

export const store = new DataStore();

Nu hebben we een enkele store die beschikbaar is voor de hele applicatie. De componenten hebben nu niet meer de booksList$ beschikbaar, maar moeten het doen via de

store.booksList$.subscribe(this);

Zo, nu zijn we weer op precies hetzelfde punt als voordat we een store hadden. Wel valt op dat we nu twee pattern gebruiken. We hebben de Observable pattern die het mogelijk maakt om te subsciben, en functionaliteit om een nieuwe set data te initialiseren. Nog steeds hebben we bereikt dat slechts een deel van de applicatie eigenaar is van de data, de rest observeert alleen maar, en vraagt de eigenaar om wijzigingen.

Nu gaan we kijken hoe componenten dat moeten doen. Bijvoorbeeld nieuwe data toevoegen aan de store:

We richten een addBook functie in, in de store, zo blijft alles bij elkaar, en die addBook functie wordt aangeroepen door de componenten die een boek willen toevoegen

store.addBook(newBook);
addBook(newBook: Book) {
    this.books.push(_.cloneDeep(newBook));
    this.booksListSubject.next(this.books);
}

De betreffende functie is erg eenvoudig. Een kopie van het boek wordt op de data-array gezet. Het is een kopie zodat de store decoupled blijft van het component. Denk aan “lodash” bij de cloneDeep functie. Vervolgens worden alle Observers van de Observable genotificeerd dat de dataset is veranderd. Wat ze ermee doen is hun zaak. Het Count-component telt het aantal boeken, het ListComponent zal zijn lijst opnieuw inrichten.

Toch is het nog niet perfect. We hebben nog steeds een interne private lijst van data nodig, en modificaties worden een op een door gegeven naar de store. Nog steeds zijn er interacties tussen componenten mogelijk via de store. We gaan daar iets aan doen. Ook hebben we nog geen delete-functie in de store. We willen ook af van de lijst van Observers die eenvoudig via de next-functie op de hoogte worden gehouden van de internal state van de store (namelijk de private books array. De books array is niet immutable en zou dus niet zichtbaar mogen zijn buiten de store.

Er komt een broadcast-functie waarin zorg wordt gedragen voor de problemen met de interne lijst die op de Componenten terecht kwam. De broadcast-functie dient ertoe om de bescherming van de lijst af te splitsen van de addBook, en later ook andere functies die voor wijzigingen zorgen. In de broadcast-functie vindt een cloneDeep aanroep plaats waardoor de interne data-array wordt afgeschermd voor de buitenwereld.

addBook(newBook:Book) {
  this.books.push(_.cloneDeep(newBook));
  this.broadcast();
}

deleteBook(deleted: Book) {
    _.remove(this.books, book => book.id === deleted.id );
    this.broadcast();
}

updateBook(update: Book) {
    const book = _.find(this.books, book => book.id === updated.id );
    book = _.cloneDeep(updated);
    this.broadcast();
}

broadcast() {
  this.booksListSubject.next(_.cloneDeep(this.books));
}

 

Een overzicht van wat we nu hebben bereikt. Laten we wel vaststellen dat dit een kleine applicatie is. In een grote applicatie zijn componenten verder verwijderd, zodat ze niet eenvoudig via inputs en outputs kunnen werken. We komen dan tot andere oplossingen. Maar voor die andere oplossingen is het goed om te begrijpen wat er in de basis belangrijk is, en dat is wat we hier zien.

  • De applicatie is veel simpeler dan in het begin
  • De applicatie werkt foutloos, de componenten lopen elkaar niet meer in de weg.
  • De data zijn opgeslagen op een centrale plaats en zijn niet van buitenaf benaderbaar.
  • De componenten zijn in staat om alle acties van de store te volgen, en daaruit conclusies voor hun eigen functionaliteit te verbinden.
  • De volgorde waarin componenten worden gecreëerd is niet meer van belang.

We zijn bijna op het punt beland waar we de RxJS library gaan introduceren.

We kunnen constateren dat de DataStore en de Observable veel dichter bij elkaar staan dan in de eerste instantie lijkt. Nu bevat de DataStore een Observable als member-variabele. Alle componenten die toegang tot de data nodig hebben doen dat via deze observable. Maar we kunnen ook zien dat de DataStore zelf een Observable is. Dit betekent dat de functies subscribe en unsubscribe op DataStore niveau moeten worden geïmplementeerd.

De DataStore komt er zo uit te zien

class DataStore implements Observable{
  private books : Book[];
  private booksListSubject = new SubjectImplementation();

  initializeBooksList(newList: Book[]) {
    this.books = _.cloneDeep(newList);
  }

  addBook(newBook: Book) {
    this.books.push(_.cloneDeep(newBook));
    this.broadcast();
  }

  broadcast() {
    this.booksListSubject.next(_.cloneDeep(this.books));
  }

  subscribe(obs: Observer) {
    this.booksListSubject.subscribe(obs);
      obs.next(this.books);
  }

  unsubscribe(obs: Observer) {
    this.booksListSubject.unsubscribe(obs);
  }
}

Het spreekt dat de componenten dan gelijk op de store subscriben, en niet meer op de store.booksList$

RxJS

Kijk nog eens naar de interfaces die we in het begin hebben gecreëerd. Observer met next, en Observable met subscribe en unsubscribe. Om verder te komen kijk naar de documentatie van RxJS, deze is geheel vernieuwd en erg duidelijk. Het gaat allemaal over Subjects, Observable en Observers. Met de kennis die we tot nu toe hebben opgedaan moet dat eenvoudig te begrijpen zijn. We gaan nog eens naar onze code kijken en zien hoe dit erin zou passen.

import * as _ from 'lodash';
import {Book} from '../shared/model/book';

import { Subject, Observable } from 'rxjs';


class DataStore {

    private books: Book[]  = [];

    private booksListSubject = new Subject<Book[]>();

    public booksList$: Observable<Book[]> = this.booksListSubject.asObservable();

    initializeBooksList(newList: Book[]) {
        this.books = _.cloneDeep(newList);
        this.broadcast();
    }

    addBook(newBook: Book) {
        this.books.push(_.cloneDeep(newBook));
        this.broadcast();
    }

    deleteBook(deleted: Book) {
        _.remove(this.books,
            book => book.id === deleted.id );
        this.broadcast();
    }

    toggleBookViewed(toggled: Book) {
        const book = _.find(this.books, book => book.id === toggled.id);

        book.completed = ! book.completed;
        this.broadcast();


    }

    broadcast() {
        this.booksListSubject.next(_.cloneDeep(this.books));
    }
}

export const store = new DataStore();

Na importeren van RxJS zoiet onze sourcecode er zo uit. We hebben alle interfaces weggedaan, niet meer nodig. We hebben de Subject implementatie weggedaan, die zit in RxJS. We houden het Subject private. Een gouden regel is dat het Subject altijd aanwezig is als er een Observable is, en dat het Subject altijd private is. Het Subject is wat Observable is. We zien dat RxJS is een simpele toolkit om het werk te vervangen dat wij hadden gedaan.
We moeten nog de componenten updaten. Ze subscriben en updaten op deze wijze (Lijstcomponent)

ngOnInit() {
    store.booksList$.subscribe((books: Book[]) => {
      this.books = books;
    });
}

next(data:Book[]) {
    console.log('Books list component received data ..');
    this.books = data;
}

 

Als laatste stap halen we uit de DataStore de schaduwlijst weg, die is een bron van bugs, en we vervangen Subject door BehaviorSubject. BehaviorSubject onthoudt de laatste waarde die gebroadcast is, zodat componenten die net opkomen een waarde krijgen, en niet afhankelijk zijn van de laatste broadcast. De DataStore komt er als volgt uit te zien:

class DataStore {

    private booksListSubject = new BehaviorSubject([]);

    public booksList$: Observable<Book[]> = this.booksListSubject.asObservable();

    initializeBooksList(newList: Book[]) {
      const books = _.cloneDeep(newList);
      this.booksListSubject.next(books);
    }

    addBook(newBook: Book) {
      console.log("adding book")
      const books = this.cloneBooks();
      books.push(_.cloneDeep(newBook));
      this.booksListSubject.next(books);
    }

    deleteBook(deleted: Book) {
      const books = this.cloneBooks();
      _.remove(books,
            book => book.id === deleted.id );
      this.booksListSubject.next(books);
    }

    toggleBookViewed(toggled: Book) {
      const books = this.cloneBooks();
      const book = _.find(books, book => book.id === toggled.id);

      book.completed = ! book.completed;
      this.booksListSubject.next(books);
    }

    private cloneBooks(){
      return _.cloneDeep(this.booksListSubject.getValue());
    }
}

Ter afsluiting, wat hebben we bereikt:

  • We hebben het Observable Pattern geimplementeerd.
  • Op deze wijze kunnen modules en componenten over een data-source beschikken en samenwerken op een ontkoppelde wijze.
  • Iedere module reageert op het aankomen van nieuwe data, of het verwijderen van data in een andere module, zonder met die andere module contact te hebben.
  • Het is voldoende om in te schrijven in de Datastore, en te reageren op de updates die dan aankomen.

Hierna hebben we RxJS geintroduceerd, en van daaruit dezelfde functionaliteit geleverd. RxJS zal de manier worden om reactive programming te implementeren in Angular.

Angular Components

Er zijn een aantal component-libraries die voor Angular veel worden gebruikt. Een overzicht om te helpen bij een keuze, of ter oriëntatie.

Material2

Dit is de officiele component-library die Material Design voor Angular implementeert. De library is gebouwd met Angular en Typescript. De UI componenten dienen als voorbeeld om te tonen hoe goede Angular code te schrijven.

De website van Material2 is goed voorzien van documentatie. Een handige kop is gepubliceerd als “Guides”. Met daarin de werkwijze voor

  • “Getting started”, hierin wordt uitgelegd hoe de library te installeren met NPM of Yarn
  • Schematics, deze helpen bij het configureren van een project waarin Material2 wordt gebruikt.
  • Theming Angular. Een thema (theme) is een set van kleuren en fonts gebaseerd op de Material Design Spec
  • Theming your own components, hiervoor wordt gebruik gemaakt van SASS
  • Angular Material typography, Typography maakt tekst leesbaar en aantrekkelijk. Angular’s typography is gebaseerd op de Material Design Spec
  • Customizing Angular Material component styles, er zijn 3 facetten om rekening mee te houden wanneer een Angular component te stylen: encapsulation, selector specify en component location.
  • Zelf form-velden creëren.

NGX Bootstrap / NG Bootstrap / Bootstrap

Welke bootstrap te gebruiken? De drie producten lijken in een soort onderlinge concurrentie. In elk geval kan men zeggen dat de originele Bootstrap afhankelijk is van jQuery, er vindt dus DOM manipulatie plaats buiten Angular om. Dat is extra complex, en moet met voorzichtigheid worden gebruikt. Toch wordt deze library veel gebruikt, maar dat zal ook met installed base te maken hebben. Ngx-bootstrap (Angular 2+) ondersteunt bootstrap 3 en bootstrap 4, terwijl ng-bootstrap alleen bootstrap 4 ondersteunt. Mocht men dus ondersteuning willen voor bootstrap 3 elementen, en geen installed base moeten onderhouden, dan lijkt ngx-bootstrap de beste keuze. Echter ngx-bootstrap heeft wel de bootstrap.min.css file als afhankelijkheid, en voor dat doel moet dus ook bootstrap worden geïnstalleerd. Ng-bootstrap is heel expliciet volledig onafhankelijk van enige bootstrap installatie. Het is een doel van de ontwikkelaars. Dus als men geen bootstrap 3 elementen wil, en als men geen oude installed base hoeft te onderhouden, dan is ng-bootstrap (alleen Angular 5+) de beste keuze. Simpel, eenduidig, goed gedocumenteerd en goed onderhouden.

Prime NG

Deze zeer uitgebreide componenten library bevat vele tientallen componenten. Er zijn ook componenten die andere libraries niet hebben, en dat maakt het speciaal. Ik noem maar een voorbeeld: de codehighlighter (zie afbeelding). De library is ontwikkeld door PrimeTek Informatics, een in Turkije gevestigd bedrijf naast universiteitsgebouwen van Ankara. Het bedrijf is gespecialiseerd in open source libraries. PrimeNG wordt gebruikt door Volkswagen, Ford, Lufthansa, ebay, dat zegt wel iets over schaalbaarheid en performance.

Onsen UI

Onsen UI is een populaire library voor hybride en mobile web-apps voor Android en iOS. Onsen UI is een eigenzinnig buitenbeentje van goede kwaliteit en beschikbaar voor meerdere platformen, zowel tijdens ontwikkeling als tijdens gebruik. Een grote varieteit aan UI componenten geschikt voor mobiele apps. Tabs, menu’s, navigatie, lijsten, formulieren en veel meer. Ze zijn allemaal voorzien van iOS en Android Material design support, en de stijl past zich automatisch aan, aan de app, gebaseerd op het platform. Binnen dezelfde code is het mogelijk om iOS en Android te ondersteunen.

Vaadin Components

Vaadin Components vult de kloof tussen Angular en Polymer. De Vaadin componenten zijn geïnspireerd door Material en dienen voor mobile en desktop. De elementen zijn verwerkt in verschillende repos.

NG-ZORRO

NG-ZORRO is in Typescript geschreven, de componenten vormen een Angular implementatie van het befaamde Chinese Ant-Design (waarmee Alibaba is gecreeerd.)

NG-Lightning

Angular componenten die gebouwd zijn voor het Saleforce Lightning Design System. De componenten zijn stateless en vertrouwen op hun input-eigenschappen. Daardoor zijn ze goed presterend en erg flexibel.

NG-Semantic-UI

De bouwblokken van Semantic-UI voor Angular. Het zijn 27 componenten.

Clarity

Clarity is een open source design systeem ontwikkeld door VMware. Het systeem brengt UX ontwikkelrichtlijnen en een html/css framework samen met Angular componenten. Het bestaat uit een rijke set data gebonden componenten on top van Angular.

Welke wordt het meeste gebruikt?

Duidelijk is dat Angular-material met grote overmacht wint. Dat lijkt mij een goede reden om daarvoor te kiezen. Er is veel kennis en veel ondersteuning op Stackoverflow. NG-bootstrap is echter een sterke tweede. Hoewel NG-Zorro veel minder wordt gebruikt, heeft het zijn schaalbaarheid wel goed bewezen. Het zou best wel kunnen dat deze de nieuwe hit met stip gaat worden.

NGX-Charts

Wilt u een mooie data-presentatie op Internet? Wilt u die zelf bepalen of mede bepalen? Dan is NGX-charts mogelijk wat u zoekt.

Angular D3 Chart Framework heeft zeer veel mogelijkheden, soorten van grafiek, kleurenschema’s.

NGX-Charts is uniek, het is niet een wrapper voor D3, noch van enige andere chart-engine. Het gebruikt Angular om de SVG elementen te renderen, met instandhouding van snelheid en data-binding. Het gebruikt D3 voor zijn excellente mathematische functies, schaal, en assen. Door het renderen aan Angular over te laten open zich een eindeloze serie van mogelijkheden, zoals AoT (Ahead of Time compilation). Data-visualisatie is wetenschap, maar het kan ook mooi zijn. Bij het creëren van de elementen is veel aandacht geweest voor esthetiek. De stijlen zijn echter ook nog volledig aan te passen via CSS.

Ik laat hieronder een aantal grafiektypen en kleurenschema’s zien, maar het is nog geen 10% van wat NGX-Charts kan. Het geeft echter wel aan hoe mooi de vormen en  kleuren op elkaar zijn afgestemd.

En dan wat voorbeelden van kleurschema’s, en bedenk, iedere combinatie is mogelijk, en er zijn nog veel meer mogelijkheden.

 

Material Design componenten voor Angular

Sprint van Zero tot App.
Creëer je app met uitgebreide, moderne UI componenten die zowel als web-app, mobile en tablet werken.
Snel en consistent,
Tot in detail afgestemd op performance, want iedere seconde telt. Het is getest in alle gangbare browsers.
Veelzijdig,
Vanwege thema-gebaseerd werken is het mogelijk om de look en feel consistent te houden aan het merk
Geoptimaliseerd voor Angular
Gebouwd door het Angular team voor naadloze integratie

Material Design voorbeelden.

Material Design Voorbeelden

De Material Design voorbeelden tonen de flexibiliteit van Material Theming en componenten om expressieve en unieke applicaties te creëren.

De Material Design Studies kunnen gebruikt worden als inspiratie. De studies onderzoeken via fictieve apps, elk met unieke eigenschappen en use-cases hoe meerdere ontwerpbeslissingen worden genomen en hoe verschillende merken zich uitdrukken in productcategorieën waaronder muziek, detailhandel, productiviteit, financiën, on demand services en onderwijs.

Expressief:

Door de mogelijkheden te tonen die Material Theming biedt, drukt elke studie een ander merk uit.

Basil

Basil is een recept-app die Material Design componenten en Material Theming gebruikt om een merk-ervaring te creëren, die aanspreekt en herkenbaar is.

Basil kan worden gebruikt om door recepten te grasduinen die zijn samengesteld door chef-koks. Het merk dient benaderbaar, dichtbij, direct en verrassend te zijn. Basil’s gedurfde typografie en kleur worden gecombineerd met een eenvoudige benadering van de inhoud. De app is toegankelijk en daagt uit tot verkenning. Basil’s gedurfde typografie en kleur worden gecombineerd met eenvoud van benadering van de inhoud. De app is aantrekkelijk om te verkennen, en gemakkelijk te begrijpen.

Gedetailleerde informatie over Basil: https://material.io/design/material-studies/basil.html

Crane

Crane is een reis-app die Material Design-componenten en Material Theming gebruikt 

Crane is een reis-app waarmee gebruikers reis-opties, accommodaties en restaurants kunnen vinden en boeken die overeenkomen met hun voorkeuren. De Crane-app is zowel functioneel (gebruikt voor reserveringen) als informatief (zodat gebruikers nieuwe ervaringen kunnen verkennen). Het merk van Crane is verfijnd en expressief, met aandacht voor visuele en bewegingsdetails die een op maat gemaakte belevenis is voor de gebruiker.

De Crane-app is zowel functioneel alsook informatief, het biedt een verscheidenheid aan filters om de inhoud aan te passen, zodat deze relevant is voor de gebruiker. Om de flexibiliteit toegankelijk te houden zijn interacties gecentreerd rondom een achtergrond-component. Wijzigingen in voorkeuren of filters in de achtergrond hebben onmiddellijk effect op de voorste lagen. Bijvoorbeeld wanneer een gebruiker een vlucht zoekt, en de informatie invoert in de achtergrondlaag, dan worden vluchten op de voorgrondlaag getoond die aan de criteria van de achtergrondlaag voldoen.

Gedetaillerde informatie over het Crane-design op https://material.io/design/material-studies/crane.html

Fortnightly

Fortnightly is een nieuws-app.

Fortnightly is een app die het nieuws over verschillende onderwerpen behandelt. De nadruk van de app ligt op de inhoud en de fotografie.

Fortnightly is ontworpen om een ​​verscheidenheid aan inhoud te bieden op een manier die de inhoud zelf in de aandacht van de gebruikerservaring plaatst. Het merk komt tot uiting in het logo, het typografische systeem en de verschijningen van paars (de secundaire kleur van de app) in een app met een andere grijstint.

Fortnightly is content-focussed en legt nadruk op de inhoud door het grijswaardenpalet functioneel in te zetten. Het layout-raster laat zich inspireren door de kranten op traditionele wijze weer te geven.

Meer info op https://material.io/design/material-studies/fortnightly.html

Owl

Owl is een educatieve app die Material Design-componenten en Material Theming gebruikt om een ​​energieke, motiverende merkervaring te creëren.

Owl is een educatieve app die cursussen aanbiedt voor mensen die nieuwe vaardigheden op het gebied van design, kunst, architectuur of mode willen ontdekken en leren. Owl gebruikt gedurfde kleuren, vormen en typografie om  deze kenmerken te benadrukken: energie, durf en plezier.

Owl’s ontwerp weerspiegelt de energie en opwinding van het leren van een nieuwe vaardigheid, het drukt met behulp van een gedurfde esthetiek onderzoek en groei uit. De UI bevat niet-gevulde vormen die de gebruiker uitnodigen om ze te vullen met nieuwe inhoud en cursussen. Het copywriting spreekt met een uitnodigende, heldere toon.

Zie https://material.io/design/material-studies/owl.html voor meer info.

Rally

Rally is een personal finance-app die Material Design-componenten en Material Theming gebruikt om een ​​merk ervaring te creëren door een datagedreven esthetiek.

Rally is een app voor persoonlijke financiën die koopgedrag registreert en inzichten en waarschuwingen genereert met betrekking tot uw financiën.

Rally is ontworpen om een ​​grote hoeveelheid dicht op elkaar geplaatste informatie op een overzichtelijke manier weer te geven. Gebruikers worden uitgenodigd om patronen in de gepresenteerde gegevens te identificeren door middel van kruisverwijzingen en onderzoek van informatie.

De donkergrijze gebruikersinterface geeft Rally een eersteklas gevoel, terwijl de heldere accentkleuren ervoor zorgen dat de gegevens opvallen tegen de achtergrond. Het hoge contrast tussen gegevens en de achtergrondkleur maakt het gemakkelijk om de grafieken en diagrammen ervan te lezen.

Zie ook https://material.io/design/material-studies/rally.html

Reply

Reply is een e-mailapp die Material Design-componenten en Material Theming gebruikt om een ​​communicatie-ervaring te creëren.

Reply is een app die individuen en groepen helpt communiceren. Het is ontworpen voor duidelijkheid, leesbaarheid, intuïtie en gebruiksgemak.

Het merk projecteert vriendelijkheid, competentie en een vleugje eigenzinnigheid.

Als gevolg hiervan geeft het ontwerp van de app prioriteit aan functionele eigenschappen, elementen die het gebruikersgemak dienen verdringen de design-elementen die geen functioneel doel hebben.

De branding van Reply wordt vaak gecombineerd met gebruikersacties, zoals de navigatie-laag die wordt benaderd vanuit het merklogo.

Meer info op: https://material.io/design/material-studies/reply.html

Shrine

De Shrine-app biedt een online marktplaats met lifestyle- en modeartikelen van gepromote labels. Shrine’s merkesthetiek is modern, elegant en verfijnd. Shrine is het verenigende concept achter de verschillende merken en producten die worden getoond.

Shrine gebruikt een minimalistische esthetiek en creëert een ervaring waarbij inhoud en acties de voorhoede vormen van de gebruikerservaring. Het merk Shrine speelt een belangrijke rol als centrale, verbindende factor voor de verscheidenheid van producten en merken die worden getoond.

Schuine sneden zijn een visueel thema in de Shrine-app en worden gebruikt op verschillende componenten en elementen. Ze weerspiegelen de vorm van het logo van Shrine en fungeren als een verlengstuk van het merk.

Lees meer over de ontwerp-details: https://material.io/design/material-studies/shrine.html