Skip to content

3. De Angular-client JS

3.1. Referenties voor het Angular-framework JS

Aan het begin van dit document zijn twee referenties voor het Angular-framework JS vermeld. We vermelden ze hier nogmaals:

  • [ref1]: het boek "Pro AngularJS", geschreven door Adam Freeman en uitgegeven door Apress. Het is een uitstekend boek. De broncodes van de voorbeelden uit dit boek zijn gratis beschikbaar op de URL [http://www.apress.com/downloadable/download/sample/sample_id/1527/];
  • [ref2]: de officiële documentatie van Angular JS [https://docs.angularjs.org/guide];

Angular JS verdient een heel boek op zich. Het boek van Adam Freeman telt meer dan 600 pagina’s en die zijn geen verspilde moeite. We gaan een Angular-applicatie beschrijven en tijdens die beschrijving zullen we ingaan op de basisprincipes van dit framework. We zullen ons echter beperken tot de uitleg die nodig is om de voorgestelde oplossing te begrijpen. Angular is een uiterst veelzijdig framework en er bestaan talrijke manieren om tot hetzelfde resultaat te komen. Dit vormt een uitdaging, want als je net begint, weet je niet of je een oplossing gebruikt die beter of slechter is dan een andere. Dat geldt ook voor de hier voorgestelde oplossing. Deze zou anders geschreven kunnen worden, wellicht volgens betere praktijken.

3.2. Architectuur van de Angular-client

De architectuur van de Angular-client lijkt op die van een klassieke webapplicatie MVC, met enkele verschillen. Een Spring-webapplicatie MVC heeft bijvoorbeeld de volgende architectuur:

De verwerking van een verzoek van een client verloopt als volgt:

  1. verzoek – de aangevraagde URL hebben de vorm http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... De [Dispatcher Servlet] is de Spring-klasse die de binnenkomende URL verwerkt. Deze „routeert“ de URL naar de actie die deze moet verwerken. Deze acties zijn methoden van specifieke klassen die [Contrôleurs] worden genoemd. De C van MVC is hier de tekenreeks [Dispatcher Servlet, Contrôleur, Action]. Als er geen actie is geconfigureerd om de binnenkomende URL te verwerken, zal de servlet [Dispatcher Servlet] antwoorden dat de gevraagde URL niet is gevonden (fout 404 NOT FOUND);
  1. verwerking
  • De gekozen actie kan gebruikmaken van de parameters parami die de servlet [Dispatcher Servlet] aan haar heeft doorgegeven. Deze kunnen uit verschillende bronnen afkomstig zijn:
    • van het pad [/param1/param2/...] van URL,
    • de parameters [p1=v1&p2=v2] van URL,
    • parameters die door de browser bij het verzoek zijn meegestuurd;
  • bij de verwerking van het verzoek van de gebruiker kan de actie de laag [metier] [2b] nodig hebben. Zodra het verzoek van de klant is verwerkt, kan dit verschillende reacties oproepen. Een klassiek voorbeeld is:
    • een foutpagina als het verzoek niet correct kon worden verwerkt
    • een bevestigingspagina in het andere geval
  • de actie vraagt om een bepaalde weergave [3] weer te geven. Deze weergave toont gegevens die het weergavemodel worden genoemd. Dit is de M van MVC. De actie zal dit model M [2c] aanmaken en vragen om een weergave V weer te geven [3];
  1. antwoord – de gekozen weergave V gebruikt het door de actie opgebouwde model M om de dynamische delen van het antwoord HTML te initialiseren dat zij naar de client moet verzenden, en verstuurt vervolgens dit antwoord.

De architectuur van onze Angular-client zal vergelijkbaar zijn, maar met iets andere terminologie. Allereerst zijn Angular-applicaties doorgaans webapplicaties met één pagina (APU) of Single Page Applications (SPA):

Image

  • De gebruiker vraagt de initiële URL van de applicatie aan in de vorm: http://machine:port/contexte. De browser zal een webserver benaderen om het gevraagde document op te halen. Dit is een pagina die is opgemaakt met HTML en dynamisch wordt gemaakt door JavaScript;
  • vervolgens gaat de gebruiker interactief om met de weergaven die hem worden getoond. Er zijn verschillende soorten interacties te onderscheiden:
    • interacties die geen interactie met de buitenwereld vereisen, bijvoorbeeld het verbergen of weergeven van elementen in de weergave. Deze worden afgehandeld door de ingebouwde JavaScript-code;
    • interacties die gegevens vereisen van een externe webservice. Deze worden opgehaald via een AJAX-aanroep (Asynchronous JavaScript and XML), waarna een model wordt opgebouwd en een weergave wordt getoond;
    • interacties die een andere weergave vereisen dan de oorspronkelijke weergave. Deze wordt opgevraagd via een Ajax-aanroep naar de server die de oorspronkelijke pagina heeft geleverd. Vervolgens herhaalt het voorgaande proces zich. De verkregen pagina wordt in de cache van de browser opgeslagen. Bij de volgende aanroep wordt deze niet opnieuw opgevraagd bij de externe server via de HTML-aanroep;

Uiteindelijk voert de browser slechts één HTTP-verzoek uit, namelijk dat waarmee de startpagina wordt opgehaald. De volgende HTTP-verzoeken, naar de paginaserver HTML of naar externe webservices, worden uitgevoerd door de in de pagina’s ingebedde JavaScript-code.

We presenteren nu de architectuur van de applicatie binnen de browser. We laten de server HTML buiten beschouwing, die de pagina’s HTML van de applicatie levert. Voor de uitleg kunnen we ervan uitgaan dat ze allemaal in de cache van de browser aanwezig zijn.

Allereerst moeten we deze architectuur in kaart brengen:

  • in [1] bevinden we ons in een browser;
  • in [2] heeft een gebruiker interactie met de weergaven die door de browser worden getoond;
  • in [3] worden de gegevens opgehaald uit het netwerk, vaak via webservices;

De gebruiker heeft interactie met weergaven: hij vult formulieren in en verzendt ze. Laten we dit proces toelichten aan de hand van de bovenstaande weergave V1. We gaan ervan uit dat dit de startweergave van de applicatie is. Deze is op de volgende manier verkregen:

  • de gebruiker vraagt de initiële weergave URL van de applicatie aan in de vorm: http://machine:port/contexte;
  • de browser heeft het document opgevraagd dat bij deze URL hoort. Hij ontving de pagina HTML / CSS / JS vanuit de weergave V1;
  • het in de pagina ingebedde JavaScript nam vervolgens het heft in handen en gaf de controle door aan de controller C1 [5];
  • deze heeft het model M1 [8] [9] van de weergave V1 opgebouwd. Voor het bouwen van dit model was mogelijk het gebruik van interne diensten [6] en het raadplegen van externe diensten [7] nodig;

De gebruiker heeft nu een weergave V1 voor zich. Laten we aannemen dat dit een formulier is. Hij vult het in en bevestigt het vervolgens:

  • in [4] verzendt de gebruiker het formulier;
  • in [5] wordt deze gebeurtenis verwerkt door een van de methoden van de controller C1;

Als de gebeurtenis slechts een eenvoudige wijziging in de weergave V1 tot gevolg heeft (velden verbergen/weergeven), zal de controller C1 het model M1 van de weergave V1 aanpassen en vervolgens de weergave V1 opnieuw weergeven. Hiervoor kan hij een van de services van de laag [services] [6] nodig hebben.

Als voor de gebeurtenis externe gegevens nodig zijn:

  • in [6] zal de controller C1 de laag [DAO] vragen om deze gegevens op te halen;
  • in [7] zal deze laag een of meer AJAX-aanroepen doen om deze gegevens op te halen;
  • in [8] en [9] wordt het model M1 gewijzigd en wordt de weergave V1 weergegeven;

Als de gebeurtenis in de twee voorgaande gevallen tot een wijziging van de weergave leidt, zal de controller C1 – in plaats van de weergave V1 weer te geven – een nieuwe URL [10] opvragen. Dit is een interne URL van de browser. Dit leidt niet onmiddellijk tot een HTTP-aanroep naar de HTML-pagina-server. Deze wijziging van URL wordt verwerkt door een router die zo is geconfigureerd dat aan elke interne URL een weergave V en de bijbehorende controller C zijn gekoppeld. De router zorgt er vervolgens voor dat de nieuwe weergave Vn wordt weergegeven. Vóór de weergave neemt de bijbehorende controller Cn het over, bouwt het model Mn op en laat vervolgens de weergave Vn [11] weergeven. Als de pagina HTML van de weergave Vn niet in de cache van de browser stond, wordt deze opgevraagd bij de paginaserver HTML.

De laag [Présentation] van deze architectuur lijkt sterk op de architectuur JSF (Java Server Faces):

  • de weergave V komt overeen met de Facelet-weergave van JSF;
  • de controller C komt overeen met de bean JSF, een Java-klasse die zowel het model M van de weergave V als de gebeurtenishandlers daarvan bevat;

De laag [Services] verschilt van de [Services]-lagen die we gewend zijn. Bij server-side webontwikkeling hebben we meestal de volgende gelaagde architectuur:

Hierboven communiceert de laag [web] alleen via de laag [métier] met de laag [DAO]. Niets zou ons ervan weerhouden om in de laag [web] een verwijzing naar de laag [DAO] in te voegen, waardoor deze communicatie mogelijk zou worden. Maar dat doen we niet.

Met Angular leggen we onszelf die beperking niet op. De architectuur ziet er dan als volgt uit:

  • in [1] kan de laag [présentation] rechtstreeks communiceren met elke willekeurige service;
  • in [2] kennen de services elkaar. Een service kan gebruikmaken van een of meerdere andere services.

3.3. De weergaven van de Angular-client

De Angular-clientviews zijn al besproken in paragraaf 1.3.3. Om het lezen van dit nieuwe hoofdstuk te vergemakkelijken, geven we ze hier nogmaals weer. De eerste view is als volgt:

  • in [6], de startpagina van de applicatie. Het betreft een applicatie voor het maken van afspraken bij artsen;
  • in [7], een selectievakje waarmee je al dan niet in de modus [debug] kunt zijn. Deze laatste wordt gekenmerkt door de aanwezigheid van het kader [8] dat het sjabloon van de huidige weergave weergeeft;
  • in [9], een kunstmatige wachttijd in milliseconden. Deze is standaard 0 (geen wachttijd). Als N de waarde van deze wachttijd is, wordt elke actie van de gebruiker uitgevoerd na een wachttijd van N milliseconden. Zo kun je zien hoe de applicatie het wachten afhandelt;
  • in [10], de URL van de Spring 4-server. Als we het voorgaande volgen, is dit [http://localhost:8080];
  • in [11] en [12], de gebruikersnaam en het wachtwoord van degene die de applicatie wil gebruiken. Er zijn twee gebruikers: admin/admin (login/wachtwoord) met een rol (ADMIN) en user/user met een rol (USER). Alleen de rol ADMIN heeft het recht om de applicatie te gebruiken. De rol USER is er alleen om te laten zien wat de server in dit gebruiksscenario antwoordt;
  • in [13], de knop waarmee je verbinding kunt maken met de server;
  • in [14], de taal van de applicatie. Er zijn er twee: standaard Frans en Engels.
  • in [1] wordt verbinding gemaakt;
  • zodra je bent ingelogd, kun je de arts kiezen bij wie je een afspraak wilt maken [2] en de dag waarop die plaatsvindt [3];
  • je vraagt in [4] om de agenda van de gekozen arts voor de gekozen dag te bekijken;
  • zodra je de agenda van de arts hebt ontvangen, kun je een tijdvak reserveren [5];
  • in [6] kiest men de patiënt voor de afspraak en bevestigt men deze keuze in [7];

Zodra de afspraak is bevestigd, keert men automatisch terug naar de agenda, waar de nieuwe afspraak nu is opgenomen. Deze afspraak kan later worden verwijderd via [7].

De belangrijkste functies zijn nu beschreven. Ze zijn eenvoudig. De functies die niet zijn beschreven, zijn navigatiefuncties om terug te keren naar een vorige weergave. Laten we afsluiten met het taalbeheer:

  • in [1] schakelt u over van het Frans naar het Engels;

2

Image

  • naar [2], de weergave wordt in het Engels weergegeven, inclusief de kalender;

3.4. Configuratie van het Angular-project

We gaan onze Angular-client stap voor stap opbouwen. We gebruiken Webstorm.

Laten we een lege map [rdvmedecins-angular-v1] aanmaken en deze vervolgens openen met Webstorm:

  • in [1] openen we een map;
  • in [2] selecteren we de map die we hebben aangemaakt;
  • in [3] krijgen we een leeg WebStorm-project;
  • in [4] wordt het project geconfigureerd via de optie [File / Settings];
  • In [5] en [6] wordt de eigenschap [Spelling] geconfigureerd, die de spellingcontrole regelt. Standaard is deze ingeschakeld. Aangezien de gedownloade software in het Engels is, worden onze Franstalige opmerkingen bij de programma’s gemarkeerd als mogelijke spelfouten. We schakelen deze spellingcontrole [7] daarom uit;
  • in [8] maken we een nieuw bestand aan;
  • in [9] kiezen we ervoor om het bestand [package.json] aan te maken, dat de applicatie beschrijft met een syntaxis JSON;
  • in [10] wordt het gegenereerde bestand aangepast zoals weergegeven in [11];
  • in [12] slaan we dit bestand zowel op in [package.json] als in [bower.json];
  • in [13] wordt het project opnieuw geconfigureerd;
  • in [14], configureer je de eigenschap [Javascript / Bower] waarmee je de JavaScript-bibliotheken kunt aangeven die je nodig hebt;
  • in [15], wijs het bestand [bower.json] aan dat we zojuist hebben aangemaakt;
  • in [16] voegen we een JavaScript-bibliotheek toe;
  • in [17] worden alle downloadbare JavaScript-bibliotheken weergegeven;
  • in [18] kunnen we een filter instellen om de lijst in [17] te filteren. Hier geven we aan dat we de bibliotheek [Angular JS] willen;
  • in [19] verschijnen de kenmerken van de bibliotheek. We zien hier dat versie 1.2.18 van Angular wordt gedownload;
  • in [20] wordt deze gedownload;
  • in [21] is te zien dat deze is gedownload;
  • in [22] zien we de gedownloade versie. Dit is dus in feite versie 1.2.19;
  • in [23] zie je de nieuwste beschikbare versie;
  • in [24] worden, volgens dezelfde werkwijze als hierboven, de volgende bibliotheken gedownload:
angular-base64
om de tekenreeks "user:password" in Base64 te coderen;
angular-i18n
om de kalender te internationaliseren
angular-route
om de interne URL-elementen van de applicatie naar de juiste controller en weergave te routeren;
angular-translate
maakt de internationalisering van de weergaven mogelijk. Dit is een onafhankelijk project van Angular. Hier zullen twee talen worden gebruikt: Frans en Engels;
angular-ui-bootstrap-bower
biedt visuele componenten die compatibel zijn met Bootstrap. Hier zullen we de kalender ervan gebruiken;
bootstrap
het CSS Bootstrap-framework. Dit wordt gebruikt om de weergaven te bouwen;
footable
biedt een visuele component van het type "tabel". Deze is "responsief" in die zin dat hij zich kan aanpassen aan de schermgrootte;
bootstrap-select
biedt een component van het type "vervolgkeuzelijst";
  • in [25] zijn de gedownloade bibliotheken geïnstalleerd in de map [bower_components];
  • in [26] is te zien dat de bibliotheek JQuery is gedownload. Dit komt doordat Bootstrap deze gebruikt. Het installatiesysteem voor JavaScript-afhankelijkheden van een project is vergelijkbaar met dat van Maven voor de Java-wereld: als een gedownloade bibliotheek zelf afhankelijkheden heeft, worden deze automatisch gedownload;

Het bestand [bower.json] is gewijzigd:

{
  "name": "rdvmedecins-angular",
  "version": "0.0.1",
  "dependencies": {
    "angular": "~1.2.18",
    "angular-base64": "~2.0.2",
    "angular-route": "~1.2.18",
    "angular-translate": "~2.2.0",
    "bootstrap": "~3.1.1",
    "footable": "~2.0.1",
    "angular-ui-bootstrap-bower": "~0.11.0",
    "bootstrap-select": "~1.5.2"
  }
}

Alle gedownloade afhankelijkheden zijn in het bestand opgenomen.

3.5. De startpagina van de Angular-client

We maken een eerste versie van de startpagina van de Angular-client:

  • in [1] en [2], we maken een bestand HTML met de naam [app-01], [3] en [4];

Het bestand [app-01.html] zal voorlopig onze hoofdpagina zijn. Daarin gaan we de import configureren van de bestanden CSS en JS die de applicatie nodig heeft:


<!DOCTYPE html>
<html>
<head>
  <title>RdvMedecins</title>
  <!-- META -->
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Angular client for RdvMedecins">
  <meta name="author" content="Serge Tahé">
  <!-- de CSS -->
  <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
  <link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
  <link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
  <link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
</div>
<!-- Bootstrap-kern JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
  • regels 11-12: de bestanden CSS voor Bootstrap;
  • regel 13: het bestand CSS voor de component [boostrap-select];
  • regel 14: het bestand CSS voor de component [footable];
  • regels 21-24: de bestanden JS van de Bootstrap-componenten;
  • regel 21: de Bootstrap-componenten worden aangedreven door JQuery;
  • regel 22: het bestand JS van Bootstrap;
  • regel 23: het bestand JS voor de component [boostrap-select];
  • regel 24: het bestand JS voor de component [footable];
  • regels 26-30: de bestanden JS van Angular en de daarmee samenhangende projecten;
  • regel 26: het bestand JS van Angular. Dit moet na JQuery worden geladen als deze bibliotheek wordt gebruikt;
  • regel 27: het bestand JS van het project [angular-ui-bootstrap];
  • regel 28: het bestand JS van de router [angular-route];
  • regel 29: het bestand JS van de internationalisatiemodule voor Angular-applicaties;
  • regel 30: het bestand JS van de module [angular-base64];

De geldigheid van het bestand [app-01.html] kan worden gecontroleerd:

  • in [1] wordt gevraagd om de code te controleren;
  • in [2], het resultaat wanneer alles in orde is;

Deze systematische controle van de code vóór de uitvoering ervan wordt aanbevolen. Hier maakt deze controle het mogelijk om eventuele verwijzingsfouten in de bestanden CSS en JS op te sporen. Als een pad onjuist is, zal de code-inspector dit signaleren.

  • In [3] kan de pagina via een debugger in een browser worden geladen. In de browser krijgt men het volgende resultaat:
  • in [4] is de pagina [app-01.html] geleverd door een interne server van WebStorm die hier op poort 63342 draait;
  • bij [5]: de console van de debugger. Als er fouten waren opgetreden, zouden deze hier zijn verschenen. Hier verschijnen ook de schermuitvoer die wordt gegenereerd door de JavaScript-instructie [console.log(expression)]. We zullen deze mogelijkheid veelvuldig gebruiken;

Met de debugmodus kun je de pagina in WebStorm aanpassen en de resultaten van die wijzigingen in de browser zien zonder de pagina opnieuw te hoeven laden. Als we dus de onderstaande regel 3 toevoegen:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <h2>Version 1</h2>
</div>

en we teruggaan naar de browser, zien we dat de pagina is veranderd:

 

3.6. Kennismaking met Bootstrap

We gaan nu enkele kenmerken van Bootstrap illustreren die in de applicatie worden gebruikt. Ik heb slechts beperkte kennis van dit framework, die ik heb opgedaan door code die ik op internet heb gevonden te kopiëren en te plakken. Ik zal de rol van de klassen CSS uitleggen, voor zover ik die begrijp. Over de andere zal ik geen commentaar geven.

3.6.1. Voorbeeld 1

In Angular zijn bewerkingen die informatie van buitenaf ophalen asynchroon. Dit betekent dat de bewerking wordt gestart en dat er onmiddellijk wordt teruggekeerd naar de weergave, waarmee de gebruiker kan blijven interageren. De applicatie wordt via een gebeurtenis op de hoogte gebracht wanneer de bewerking is voltooid. Deze gebeurtenis wordt verwerkt door een functie JS die vervolgens de huidige weergave kan aanvullen of wijzigen. Als de bewerking waarschijnlijk lang gaat duren, is het nuttig om de gebruiker de mogelijkheid te bieden deze te annuleren. We zullen hem deze mogelijkheid systematisch aanbieden. Hiervoor gebruiken we een Bootstrap-banner:

Image

Om dit resultaat te verkrijgen, dupliceren we [app-01.html] in [app-02.html] en passen we de volgende regels aan:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <div class="alert alert-warning">
    <h1>Opération en cours. Veuillez patienter...
      <button class="btn btn-primary pull-right">Annuler</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
</div>
  • regel 1: de klasse CSS [container] definieert een weergavegebied binnen de browser;
  • regel 3: de klasse CSS [alert] geeft een gekleurd gebied weer. De klasse [alert-warning] gebruikt een vooraf gedefinieerde kleur;
  • regel 5: de klasse [btn] geeft een knop een uiterlijk. De klasse [btn-primary] geeft de knop een bepaalde kleur. De klasse [pull-right] plaatst de knop aan de rechterkant van de waarschuwingsbalk;
  • regel 6: een geanimeerde afbeelding die de gebruiker op de hoogte houdt;

3.6.2. Voorbeeld 2

De verschillende weergaven van de applicatie krijgen een gemeenschappelijke titel:

Image

Om dit resultaat te bereiken, dupliceren we [app-01.html] in [app-03.html] en passen we de volgende regels aan:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <!-- Bootstrap Jumbotron -->
  <div class="jumbotron">
    <div class="row">
      <div class="col-md-2">
        <img src="assets/images/caduceus.jpg" alt="RvMedecins"/>
      </div>
      <div class="col-md-10">
        <h1>Les Médecins associés</h1>
      </div>
    </div>
  </div>
</div>
  • het gekleurde gebied wordt verkregen met de klasse [jumbotron] uit regel 4;
  • regel 5: de klasse [row] definieert een regel met 12 kolommen;
  • regel 6: de klasse [col-md-2] definieert een gebied met twee kolommen in de regel;
  • regel 7: in deze twee kolommen wordt een afbeelding geplaatst;
  • regels 9-11: in de overige 10 kolommen wordt de tekst geplaatst;

3.6.3. Voorbeeld 3

De weergaven krijgen een bovenste bedieningsbalk. Daarin bevinden zich bedieningsopties, links of knoppen. Ook bevinden zich daar formulierelementen. Bijvoorbeeld:

Om dit resultaat te verkrijgen, dupliceren we [app-01.html] in [app-04.html] en wijzigen we de volgende regels:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
       <div class="navbar-collapse collapse">
        <form class="navbar-form navbar-right">
          <!-- debugmodus -->
          <label style="width: 100px">
            <input type="checkbox">
            <span style="color: white">Debug</span>
          </label>
          <!-- aanmeldingsformulier -->
          <div class="form-group">
            <input type="text" class="form-control" placeholder="Temps d'attente"
                   style="width: 150px"/>
            <input type="text" class="form-control" placeholder="URL du service web"
                   style="width: 200px"/>
            <input type="text" class="form-control" placeholder="Login"
                   style="width: 100px"/>
            <input type="password" class="form-control" placeholder="Mot de passe"
                   style="width: 100px"/>
          </div>
          <button class="btn btn-success">
            Connexion
          </button>
        </form>
      </div>
          <button class="btn btn-success">
            Connexion
          </button>
        </form>
      </div>
    </div>
  </div>
</div>
  • regel 4: de klasse [navbar] bepaalt de stijl van de navigatiebalk. De klasse [navbar-inverse] geeft de navigatiebalk een zwarte achtergrond. De klasse [navbar-fixed-top] zorgt ervoor dat wanneer je door de pagina scrolt die door de browser wordt weergegeven, de navigatiebalk bovenaan het scherm blijft staan;
  • regels 6-14: definiëren het gebied [1]. Dit is typisch een reeks klassen die ik niet begrijp. Ik gebruik de component zoals hij is;
  • regel 15: definieert een ‘responsief’ gebied van de bedieningsbalk. Op een smartphone verdwijnt dit gebied in een menu;
  • regel 16: de klasse [navbar-form] geeft een formulier in de bedieningsbalk een opmaak. De klasse [navbar-right] plaatst het rechts daarvan;
  • regels 23-32: de vier invoervelden van het formulier uit regel 17, [3]. Ze bevinden zich binnen een klasse [form-group] die de elementen van een formulier opmaakt en elk van hen heeft de klasse [form-control];
  • regel 33: de klasse [btn] die we al eerder zijn tegengekomen, aangevuld met de klasse [btn-success] die ervoor zorgt dat deze groen wordt weergegeven;

3.6.4. Voorbeeld 4

Via de bedieningsbalk kan de taal worden gewijzigd met behulp van een vervolgkeuzelijst:

Image

Om dit resultaat te verkrijgen, dupliceren we [app-01.html] in [app-05.html] en voegen we de volgende regels toe aan de bedieningsbalk:


          <button class="btn btn-success">
            Connexion
          </button>
          <!-- talen -->
          <div class="btn-group">
            <button type="button" class="btn btn-danger">
              Langues
            </button>
            <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
              <span class="caret"></span>
              <span class="sr-only">Toggle Dropdown</span>
            </button>
            <ul class="dropdown-menu" role="menu">
              <li>
                <a href="">Français</a>
              </li>
              <li>
                <a href="">English</a>
              </li>
            </ul>
          </div>
</form>

De toegevoegde regels zijn de regels 4-21.

  • regel 5: de klasse [btn-group] geeft een groep knoppen een opmaak. Er zijn er twee op de regels 6 en 9;
  • regels 6-8: de eerste knop bepaalt de tekst van de vervolgkeuzelijst. De klasse [btn-danger] geeft deze een rode kleur;
  • regels 9-12: de tweede knop is die van de vervolgkeuzelijst. Deze staat direct naast de eerste, waardoor het lijkt alsof het één component is;
  • regel 10: toont het pijltje naar beneden dat aangeeft dat de knop een vervolgkeuzelijst is;
  • regel 11: voor 'schermlezers';
  • regels 13-20: de items in de vervolgkeuzelijst zijn de items van een ongeordende lijst;

3.6.5. Voorbeeld 5

Om een formulier te verzenden of te navigeren, beschikt de gebruiker in de bedieningsbalk over opties of knoppen zoals hieronder weergegeven:

Er zijn menuopties geïmplementeerd in [1]. Om dit resultaat te bereiken, dupliceren we [app-01.html] in [app-06.html] en voegen we de volgende regels toe:


<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
...
      </div>
      <!-- menuopties -->
      <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="">
              <span>Home</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Agenda</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Valider</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Annuler</span>
            </a>
          </li>
        </ul>
        <!-- knoppen rechts -->
        <form class="navbar-form navbar-right" role="form">
...
        </form>
      </div>
    </div>
  </div>
</div>
  • De menuopties worden gegenereerd door de regels 8-29. Ook dit zijn elementen van een <ul>-lijst. De klasse [active] zorgt ervoor dat de tekst gemarkeerd wordt, wat aangeeft dat er op de optie geklikt kan worden.

3.6.6. Voorbeeld 6

We zullen de artsen en klanten in vervolgkeuzelijsten weergeven, zoals hieronder:

 

De gebruikte dropdownlijst is geen native Bootstrap-component. Het is de component [bootstrap-select] (http://silviomoreto.github.io/bootstrap-select/). Om dit resultaat te bereiken, dupliceren we [app-01.html] naar [app-07.html] en voegen we de volgende regels toe:


<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>

</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <h2><label for="medecins">Médecins</label></h2>
  <select id="medecins" data-style="btn btn-primary" class="selectpicker">
    <option value="1">Mme Marie PELISSIER</option>
    <option value="1">Mr Jacques BROMARD</option>
    <option value="1">Mr Philippe JANDOT</option>
    <option value="1">Mme Justine JACQUEMOT</option>
  </select>
</div>
<!-- Bootstrap-kern JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- lokaal script -->
<script>
  $('.selectpicker').selectpicker();
</script>
</body>
</html>
  • regel 5: het stylesheet van [bootstrap-select] moet worden geïmporteerd;
  • regel 13: het attribuut [data-style] wordt gebruikt door [bootstrap-select]. Het dient om de vervolgkeuzelijst een stijl te geven. Hier geven we het de vorm van een blauwe knop [btn-primary];
  • regel 13: het attribuut [class] wordt gebruikt in regel 23. Kan willekeurig zijn;
  • regels 14-17: de elementen van de vervolgkeuzelijst. Hier staan de klassieke HTML-tags;
  • regel 22: de JS moet worden geïmporteerd vanuit [bootstrap-select];
  • regels 24-26: een script JS dat wordt uitgevoerd zodra de pagina is geladen;
  • regel 25: een instructie JQuery. De methode [selectpicker] (selectpicker()) wordt toegepast op alle elementen met de klasse [selectpicker] ($('.selectpicker')). Er is er maar één, de tag <select> op regel 13. De methode [selectpicker] komt uit het bestand JS waarnaar op regel 22 wordt verwezen;

3.6.7. Voorbeeld 7

Om de agenda van een arts weer te geven, gebruiken we een ‘responsieve’ tabel uit de bibliotheek JS [footable]:

  • in [1]: de tabel met een normale weergave;
  • in [2]: de tabel wanneer het browservenster wordt verkleind. De kolom [Action] wordt automatisch naar de volgende regel verplaatst. Dit wordt een ‘responsieve’ of simpelweg aanpasbare component genoemd.

We dupliceren [app-01.html] naar [app-08.html] en voegen de volgende regels toe:


...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div class="row alert alert-warning">
    <div class="col-md-6">
      <table id="creneaux" class="table">
        <thead>
        <tr>
          <th data-toggle="true">
            <span>Créneau horaire</span>
          </th>
          <th>
            <span>Client</span>
          </th>
          <th data-hide="phone">
            <span>Action</span>
          </th>
        </thead>
        <tbody>
        <tr>
          <td>
            <span class='status-metro status-active'>
              9h00-9h20
            </span>
          </td>
          <td>
            <span></span>
          </td>
          <td>
            <a href="" class="status-metro status-active">
              Réserver
            </a>
          </td>
        </tr>
        <tr>
          <td>
            <span class='status-metro status-suspended'>
              9h20-9h40
            </span>
          </td>
          <td>
            <span>Mme Paule MARTIN</span>
          </td>
          <td>
            <a href="" class="status-metro status-suspended">
              Supprimer
            </a>
          </td>
        </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
  • de regels 2 en 60 zijn al aanwezig in [app-01.html]. Dit zijn de bestanden CSS en JS die worden geleverd door de bibliotheek [footable];
  • regel 3 verwijst naar het volgende bestand CSS:

@CHARSET "UTF-8";

#th-kolommen {
    text-align: center;
}

#td-kolommen {
    text-align: center;
    font-weight: bold;
}

.status-metro {
  display: inline-block;
  padding: 2px 5px;
  color:#fff;
}

.status-metro.status-active {
  background: #43c83c;
}

.status-metro.status-suspended {
  background: #fa3031;
}

De stijlen [status-*] zijn afkomstig uit een voorbeeld van het gebruik van de tabel [footable] dat op de website van de bibliotheek te vinden is.

  • regel 8: plaatst de tabel in een regel [row] en een gekleurd kader [alert alert-warning];
  • regel 9: de tabel beslaat 6 kolommen [col-md-6];
  • regel 10: de tabel HTML wordt opgemaakt met Bootstrap [class='table'];
  • regel 13: het attribuut [data-toggle] geeft aan in welke kolom het symbool [+/-] staat waarmee de regel kan worden uit- en ingeklapt;
  • regel 19: het attribuut [data-hide='phone'] geeft aan dat de kolom verborgen moet worden als het scherm de afmetingen van een telefoonscherm heeft. Men kan ook de waarde 'tablet' gebruiken;

3.6.8. Voorbeeld 8

Om de gebruiker te helpen, maken we tooltips rond de belangrijkste onderdelen van de weergaven:

Om dit resultaat te bereiken, dupliceren we [app-01.html] in [app-09.html] en voegen we de volgende regels toe:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
      <!-- menuopties -->
      <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="">
              <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
            </a>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>
<!-- Bootstrap-kern JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- lokaal script -->
<script>
  // --------------------- Angular-module
  angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>

De helpballonnen worden geleverd door de bibliotheek [angular-ui-bootstrap], die op haar beurt weer gebruikmaakt van de bibliotheek [angular]. Op regel 50 wordt de bibliotheek [angular-ui-bootstrap] geïmporteerd. Om de componenten van de bibliotheek [angular-ui-bootstrap] te implementeren, moeten we een Angular-module aanmaken. Dit gebeurt in de regels 52-55. Deze regels definiëren een Angular-module met de naam [rdvmedecins] (eerste parameter). Een Angular-module kan gebruikmaken van andere Angular-modules. Dit worden moduleafhankelijkheden genoemd. Ze worden in een array opgegeven als tweede parameter van de functie [angular.module]. Hier wordt de module met de naam [ui.bootstrap] geleverd door de bibliotheek [angular-ui-bootstrap]. Deze module zorgt voor de helpballonnen.

Regel 54 definieert een Angular-module. Standaard heeft dit geen effect op de pagina. We geven aan dat de pagina door Angular moet worden beheerd door deze aan een Angular-module te koppelen. Dit gebeurt in regel 2. Het attribuut [ng-app='rdvmedecins'] koppelt de pagina aan de module die in regel 54 is aangemaakt. De pagina wordt vervolgens door Angular geanalyseerd. De attributen [tooltip] worden gedetecteerd en verwerkt door de module [ui.bootstrap].

De syntaxis van de helpballon is als volgt:


 <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>

Hierboven voegen we een helpballon toe aan de tekst [Home]:

  • [tooltip]: definieert de tekst van de helpballon;
  • [tooltip-placement]: bepaalt de positie (bottom, top, left, right);

Met Angular JS kunnen nieuwe tags of attributen worden toegevoegd aan de reeds bestaande in de taal HTML. Deze uitbreiding van de taal HTML wordt gerealiseerd door middel van Angular-richtlijnen. Hier zijn de attributen [tooltip] en [tooltip-placement] attributen die zijn aangemaakt door [angular-ui-bootstrap].

3.6.9. Voorbeeld 9

Om de gebruiker te helpen bij het kiezen van de dag van een afspraak, bieden we hem een kalender aan:

Image

Net als bij de helpballonnen wordt deze kalender geleverd door de bibliotheek [angular-ui-bootstrap]. Om dit resultaat te verkrijgen, dupliceren we [app-01.html] in [app-10.html] en voegen we de volgende regels toe:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div>
    <pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
    <div class="row">
      <div class="col-md-2">
        <h4>Calendrier</h4>

        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
        </div>

        </div>
      </div>
    </div>
  </div>
</div>
...
<!-- lokaal script -->
<script>
  // --------------------- Angular-module
  angular.module("rdvmedecins", ['ui.bootstrap'])
</script>

</body>
</html>

Net als eerder is de pagina gekoppeld aan een Angular-module (regels 2 en 28). De kalender wordt gedefinieerd door de <datepicker>-tag op regel 16, die wordt gedefinieerd door de bibliotheek [angular-ui-bootstrap]:

  • [show-weeks='true']: om de weeknummers weer te geven;
  • [class='well']: om de kalender te omringen met een grijs gebied met afgeronde hoeken;
  • [ng-model='jour']: de attributen [ng-*] zijn Angular-attributen. Het attribuut [ng-model] verwijst naar gegevens die in het model van de weergave worden geplaatst. Wanneer de gebruiker op een datum klikt, wordt deze in de variabele [jour] van het model geplaatst. Deze variabele wordt gebruikt in regel 10. Met de syntaxis {{uitdrukking}} kan een uitdrukking worden geëvalueerd die bestaat uit elementen van het model. Hier geeft {{dag}} de waarde weer van de variabele [jour] in het model. Een sterk kenmerk van Angular is dat de weergave automatisch de wijzigingen in de variabele [jour] volgt. Wanneer de gebruiker dus de datums wijzigt, worden deze wijzigingen onmiddellijk weergegeven op regel 10. In het algemeen werkt het als volgt:
    • een weergave V is gekoppeld aan een model M;
    • Angular houdt het model M in de gaten en werkt de weergave V automatisch bij wanneer er een wijziging in het model M plaatsvindt;

De syntaxis {{dag|datum}} wordt een filter genoemd. Het is niet de waarde van [jour] die wordt weergegeven, maar de waarde van [jour], gefilterd door een filter met de naam [date]. Dit filter is vooraf gedefinieerd in Angular. Het dient om datums op te maken. Het accepteert parameters die het gewenste formaat specificeren. Zo geeft de uitdrukking {{dag | datum:'fullDate'}} aan dat men het volledige datumformaat wil, in dit geval [Friday, June 20, 2014], omdat de kalender standaard in het Engels is. We zullen binnenkort ingaan op de internationalisering ervan.

3.6.10. Conclusie

We hebben de elementen van het CSS Bootstrap-framework gepresenteerd die we gaan gebruiken. Dit waren passieve componenten: hun gebeurtenissen werden niet afgehandeld. Een klik op de knoppen of links had dus geen effect. Deze gebeurtenissen zullen in JavaScript worden afgehandeld. Het is mogelijk om deze taal te gebruiken zonder hulp van frameworks, maar net als aan de serverzijde zijn bepaalde frameworks aan de clientzijde onmisbaar. Dit geldt voor het Angular-framework JS, dat een nieuwe benadering introduceert voor de ontwikkeling van JavaScript-applicaties die door een browser worden uitgevoerd. We zullen dit nu presenteren.

3.7. Kennismaking met Angular JS

We gaan nu enkele kenmerken van het Angular JS-framework illustreren die in de applicatie worden gebruikt. We zijn er al een paar tegengekomen:

  • een HTML-pagina wordt aangedreven door Angular JS als er een module aan wordt gekoppeld:

<html ng-app="rdvmedecins">
  • Met Angular kun je via richtlijnen nieuwe tags en attributen HTML maken:
attributs : ng-app, ng-model, tooltip-placement, tooltip
balises : datepicker
  • Met Angular kun je filters maken:
{{jour|date:'fullDate'}}
  • Een weergave V geeft een model M weer. Angular houdt het model M in de gaten en werkt de weergave V automatisch bij wanneer er een wijziging in het model M plaatsvindt. De waarde van een variabele in het model M wordt in de weergave V weergegeven met:
{{variable}}

We gaan eerst dieper in op de implementatie van het Model-View-Controller-ontwerppatroon in Angular. Laten we nog eens de onderlinge verbanden tussen deze componenten vanuit architectonisch oogpunt op een rijtje zetten:

  • de view V1 geeft het model M1 weer dat door de controller C1 is opgebouwd. Deze laatste bevat niet alleen het model M1, maar ook de gebeurtenishandlers van de weergave V1. We bevinden ons in cyclus 5, 8, 9:
    • [5]: er vindt een gebeurtenis plaats in de weergave V1. Deze wordt verwerkt door de controller C1;
    • deze voert zijn taak uit ([6-7]) en bouwt vervolgens het model (M1, [8]);
    • [9]: de weergave V1 toont het nieuwe model M1. Zoals gezegd verloopt deze laatste stap automatisch. Er is, in tegenstelling tot in andere frameworks, geen expliciete push (C1 pusht het model M1 naar V1) of een expliciete pull (de weergave V1 haalt het model M1 op uit C1). Er vindt een impliciete push plaats die de ontwikkelaar niet ziet;
    • waarna de cyclus 5, 8, 9 weer begint;

3.7.1. Voorbeeld 1: het Angular-sjabloon MVC

We gaan verder met het voorbeeld van de kalender. We hebben de richtlijn gezien die deze genereert:


          <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>

Deze directive ondersteunt nog andere attributen dan de hierboven genoemde, waaronder het attribuut [min-date] dat de vroegste datum vastlegt die in de kalender kan worden gekozen. Dit komt ons goed van pas. Wanneer de gebruiker een afspraakdatum kiest, moet deze gelijk zijn aan of later zijn dan de huidige datum. We schrijven dan:


<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>

waarbij [dateMin] een variabele in het paginasjabloon is die de huidige datum als waarde heeft. Dit levert de volgende pagina op:

  • in [1] is het 19 juni 2014. De cursor geeft aan dat 19 juni kan worden geselecteerd;
  • in [2] geeft de cursor aan dat 18 juni niet kan worden geselecteerd;

We dupliceren [app-10.html] naar [app-11.html] en brengen de volgende wijzigingen aan:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div>
    <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
    <div class="row">
      <div class="col-md-2">
        <h4>Calendrier</h4>

        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
        </div>
      </div>
    </div>
  </div>
</div>
<!-- Bootstrap-kern JavaScript ================================================== -->
...
<!-- lokaal script -->
<script>
  // --------------------- Angular-module
  angular.module("rdvmedecins", ['ui.bootstrap']);
  // controller
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // minimale datum
        $scope.minDate = new Date();
      }]);

</script>

</body>
</html>

Laten we eerst het lokale script van de regels 26-37 bekijken:

  • regel 28: aanmaken van de module [rdvmedecins] met zijn afhankelijkheid van de module [ui.bootstrap] die de kalender levert;
  • regels 30-35: aanmaken van een controller. Deze zal het sjabloon van onze pagina bevatten. Er zal hier geen gebeurtenishandler zijn;
  • regels 30-31: de controller [rdvMedecinsCtrl] hoort bij de module [rdvmedecins]. Aan een module kunnen zoveel controllers worden toegevoegd als men wil. In onze applicatie hebben we:
    • een module voor het beheer van de applicatie;
    • één controller per weergave;
  • de tweede parameter van de functie [controller] is een array in de vorm ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)]. De laatste parameter is de functie die de controller implementeert. De parameters daarvan zijn objecten die Angular JS aan de functie zal verstrekken.

Laten we terugkeren naar de architectuur van een Angular-applicatie:

Hierboven bevat de controller C1 alle gebeurtenishandlers van de view V1, evenals het model M1 van deze view. De gebeurtenishandlers kunnen een of meer services [6] nodig hebben om hun werk te doen. We geven deze allemaal door als parameters van de constructiefunctie van de controller:

['S1', 'S2', ..., 'Sn', function(S1, S2, ..., Sn)]

De Si-services zijn singletons. Angular maakt er slechts één exemplaar van aan. Ze worden geïdentificeerd door een Si-naam. Waarom komen ze twee keer voor in de bovenstaande tabel? Tijdens de uitvoering worden de JS-scripts geminimaliseerd. In dit minimalisatieproces wordt de bovenstaande tabel:

['S1', 'S2', ..., 'Sn', function(a1, a2, ..., an)]

De parameters verliezen hun naam. Maar dat zijn juist de namen van de services. Het is dus belangrijk om deze namen te behouden. Daarom worden ze als tekenreeksen doorgegeven als parameters vóór de functie. De tekenreeksen worden tijdens het minificatieproces niet gewijzigd. Wanneer Angular de controller met de nieuwe array gaat opbouwen, vervangt het a1 door S1, a2 door S2, ... De volgorde van de parameters is dus belangrijk. Deze moet overeenkomen met de volgorde van de services die voorafgaan aan de definitie van de functie.

Laten we teruggaan naar de definitie van de controller [rdvMedecinsCtrl]:


  // controller
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // minimale datum
        $scope.minDate = new Date();
}]);
  • regels 3-4: het enige object dat in de controller wordt geïnjecteerd, is het object $scope. Dit is een vooraf gedefinieerd object dat het M-model vertegenwoordigt van de weergaven die aan de controller zijn gekoppeld. Om het model van een weergave uit te breiden, volstaat het om velden toe te voegen aan het object $scope;
  • dit gebeurt in regel 6. We maken het veld [minDate] aan met als waarde de huidige datum;

De weergave V maakt als volgt gebruik van dit model M:


<body ng-controller="rdvMedecinsCtrl">
<div class="container">
 ...
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
        </div>
...
</div>
...
  • regel 1: de hoofdtekst van de pagina wordt gekoppeld aan de controller [rdvMedecinsCtrl] via het attribuut [ng-controller]. Dit betekent dat alles wat zich binnen de tag <body> bevindt, de controller [rdvMedecinsCtrl] zal gebruiken om zijn gebeurtenissen te beheren en zijn M-model op te halen. Een pagina HTML kan afhankelijk zijn van meerdere controllers die al dan niet in elkaar genest zijn:
<div id='div1' ng-controller='c1'>
    ...
    <div id='div11' ng-controller='c11'>
    ...
    </div>
    ...
    <div id='div12' ng-controller='c12'>
    ...
    </div>
</div>

Hierboven:

  • de inhoud van [div1] (regels 1-10) geeft het sjabloon M1 weer, dat wordt beheerd door controller c1. De tags in dit gebied kunnen verwijzen naar gebeurtenishandlers van controller c1;
  • de inhoud van [div11] (regels 3-4) toont het sjabloon M11, beheerd door controller c11, maar ook het sjabloon M1. Er is sprake van modelovererving. De tags in dit gebied kunnen zowel verwijzen naar gebeurtenishandlers van controller c11 als naar gebeurtenishandlers van controller c1. Ze kunnen echter niet verwijzen naar het model M12 van controller c12, noch naar de gebeurtenishandlers daarvan. Controller c12 is namelijk niet bekend in de regels 3-5;
  • regels 7-9: hier geldt een soortgelijke redenering als eerder;

Laten we terugkeren naar de kalendercode:


<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>

Het attribuut [min-date] wordt geïnitialiseerd met de waarde [minDate] uit het model. Impliciet [$scope.minDate]. Het veld wordt altijd gezocht in het object $scope.

3.7.2. Voorbeeld 2: lokalisatie van datums

Op dit moment hebben we weinig aan de kalender, aangezien het een Engelse kalender is. Het is mogelijk om deze te lokaliseren:

  • in [1] hebben we een Franse kalender;
  • met [2] zetten we hem in het Engels;
  • met [3] de Engelse kalender;

We dupliceren de pagina [app-11.html] naar [app-12.html] en passen deze laatste vervolgens als volgt aan:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
  <div class="row">
    <!-- de kalender-->
    <div class="col-md-4">
      <h4>Calendrier</h4>

      <div style="display:inline-block; min-height:290px;">
        <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
      </div>
    </div>
    <!-- de talen -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
</html>

Er zijn maar weinig wijzigingen. Er zijn alleen de regels 21-31 toegevoegd voor de vervolgkeuzelijst met talen. Voor het eerst komen we een gebeurtenishandler tegen op de regels 27-28:

  • regel 27: het attribuut [ng-click] is een Angular-attribuut dat aangeeft welke gebeurtenishandler moet worden uitgevoerd wanneer er op het element met dit attribuut wordt geklikt. Hier wordt de functie [$scope.setLang('fr')] uitgevoerd. Deze zet de kalender in het Frans;
  • regel 28: hier wordt de kalender in het Engels weergegeven;
  • regel 35: aangezien de JavaScript-code van de controller vrij omvangrijk is, plaatsen we deze in een bestand met de naam [rdvmedecins.js];

Angular beheert de lokalisatie van de weergaven met een module genaamd [ngLocale]. De definitie van onze module [rdvmedecins] ziet er dus als volgt uit:


  // --------------------- Angular-module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);

Op regel 2 mogen we de afhankelijkheden niet vergeten, want Angular is soms niet erg nauwkeurig in zijn foutmeldingen. Het vergeten van een afhankelijkheid is daardoor bijzonder moeilijk op te sporen. Hier hebben we een nieuwe afhankelijkheid van de module [ngLocale].

Standaard ondersteunt Angular alleen de lokalisatie van datums, getallen, enzovoort, waarvoor lokale varianten bestaan. Het biedt geen ondersteuning voor de internationalisering van teksten. Hiervoor gebruiken we de bibliotheek [angular-translate]. De lokalisatie wordt afgehandeld door de bibliotheek [angular-i18n]. Deze bibliotheek bevat evenveel bestanden als er varianten zijn voor datums, getallen, enz.

  

Voor de Franse kalender gebruiken we het bestand [angular-locale_fr-fr.js] en voor de Engelse kalender het bestand [angular-locale_en-us.js]. Laten we eens kijken wat er bijvoorbeeld in het bestand [angular-locale_fr-fr.js] staat:


'use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
$provide.value("$locale", {
  "DATETIME_FORMATS": {
    "AMPMS": [
      "AM",
      "PM"
    ],
    "DAY": [
      "dimanche",
      "lundi",
      "mardi",
      "mercredi",
      "jeudi",
      "vendredi",
      "samedi"
    ],
    "MONTH": [
      "janvier",
      "f\u00e9vrier",
      "mars",
      "avril",
      "mai",
      "juin",
      "juillet",
      "ao\u00fbt",
      "septembre",
      "octobre",
      "novembre",
      "d\u00e9cembre"
    ],
    "SHORTDAY": [
      "dim.",
      "lun.",
      "mar.",
      "mer.",
      "jeu.",
      "ven.",
      "sam."
    ],
    "SHORTMONTH": [
      "janv.",
      "f\u00e9vr.",
      "mars",
      "avr.",
      "mai",
      "juin",
      "juil.",
      "ao\u00fbt",
      "sept.",
      "oct.",
      "nov.",
      "d\u00e9c."
    ],
    "fullDate": "EEEE d MMMM y",
    "longDate": "d MMMM y",
    "medium": "d MMM y HH:mm:ss",
    "mediumDate": "d MMM y",
    "mediumTime": "HH:mm:ss",
    "short": "dd/MM/yy HH:mm",
    "shortDate": "dd/MM/yy",
    "shortTime": "HH:mm"
  },
  "NUMBER_FORMATS": {
    "CURRENCY_SYM": "\u20ac",
    "DECIMAL_SEP": ",",
    "GROUP_SEP": "\u00a0",
    "PATTERNS": [
      {
        "gSize": 3,
        "lgSize": 3,
        "macFrac": 0,
        "maxFrac": 3,
        "minFrac": 0,
        "minInt": 1,
        "negPre": "-",
        "negSuf": "",
        "posPre": "",
        "posSuf": ""
      },
      {
        "gSize": 3,
        "lgSize": 3,
        "macFrac": 0,
        "maxFrac": 2,
        "minFrac": 2,
        "minInt": 1,
        "negPre": "(",
        "negSuf": "\u00a0\u00a4)",
        "posPre": "",
        "posSuf": "\u00a0\u00a4"
      }
    ]
  },
  "id": "fr-fr",
  "pluralCat": function (n) {  if (n >= 0 && n <= 2 && n != 2) {   return PLURAL_CATEGORY.ONE;  }  return PLURAL_CATEGORY.OTHER;}
});
}]);

Hierin staan de elementen waarmee een Franse kalender kan worden gemaakt:

  • regels 10-18: de tabel met de dagen van de week;
  • regels 19-32: de tabel met de maanden van het jaar;
  • regels 33-41: de tabel met de afgekorte weekdagen;
  • regels 42-55: de tabel met de maanden van het jaar in afgekorte vorm;
  • regels 56-63: datum- en tijdnotaties. In regel 62 herkennen we de notatie 'dd/mm/jj' voor Franse datums;
  • regels 65-95: informatie over het opmaken van getallen. Dit is hier niet van belang;
  • regel 96: de identificatiecode 'fr-fr' van de landinstelling van het bestand (fr-fr: Frans uit Frankrijk, fr-ca: Frans uit Canada, ...)

In het bestand [angular-locale_en-us.js] staat precies hetzelfde, maar dit keer voor het Engels van USA (en-us).

De bovenstaande code is niet erg eenvoudig te lezen. Als je goed kijkt, zie je dat al deze code de variabele [$locale] op regel 4 definieert. Door de waarde van deze variabele te wijzigen, worden datums, getallen, valuta enzovoort geïnternationaliseerd. Vreemd genoeg heeft Angular er niet voor gezorgd dat de variabele [$locale] tijdens de uitvoering kan worden gewijzigd. Deze wordt eenmalig gedefinieerd door het bestand van de gewenste locale te importeren:


<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>

Het heeft geen zin om alle bestanden van de gewenste locales te importeren, want zoals we hebben gezien doet elk bestand maar één ding: de variabele [$locale] definiëren. Het laatst geïmporteerde bestand krijgt voorrang en daarna is er geen manier meer om de locale te wijzigen.

Toen ik op internet zocht naar een oplossing voor dit probleem, heb ik er geen gevonden. Ik stel hier een oplossing voor: [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. Het idee is om de verschillende locales die we nodig hebben in een woordenboek op te nemen. Daar halen we ze vandaan wanneer we moeten wisselen. De JavaScript-code van [rdvmedecins.js] heeft de volgende opbouw:

 

Als we de definitie van de locale-instellingen weglaten, die 200 regels beslaat (regels 15-215 hierboven), is de code eenvoudig:

  • regel 6: definieert de module [rdvmedecins] en de bijbehorende afhankelijkheden;
  • regels 8-10: definieert de controller [rdvMedecinsCtrl] van de pagina;
  • regel 9: de constructiefunctie van de controller ontvangt twee parameters:
    • $scope: om het sjabloon van de weergave aan te maken;
    • $locale: dit is de variabele die de lokalisatie van de kalender beheert. Deze moet worden aangepast wanneer de taal wordt gewijzigd;
  • regel 13: de variabele [minDate] van het sjabloon wordt geïnitialiseerd met de huidige datum;
  • regel 15: definieert het woordenboek [locales]. Merk op dat we niet [$scope.locales] hebben geschreven. De variabele [locales] maakt namelijk geen deel uit van het sjabloon dat aan de weergave wordt getoond;
  • regels 15-215: definiëren een woordenboek {'fr':locale-fr-fr, 'en':locale-en-us}. De waarden [locale-fr-fr] en [locale-en-us] worden respectievelijk uit de bestanden JS, [angular-locale_fr-fr.js] en [angular-locale_en-us.js] gehaald. Het moeilijkste is om geen fouten te maken in de vele haakjes in dit woordenboek...
  • regel 217: de variabele $locale wordt geïnitialiseerd met locales['fr'], d.w.z. de Franse versie van de locale. Je kunt niet simpelweg [$locale=locales['fr']] schrijven, want daarmee wordt aan $locale het adres van locales['fr'] toegewezen. Er moet een waardekopie worden gemaakt. Dit kan met de vooraf gedefinieerde functie [angular.copy];
  • regel 219: de variabele [jour] van het sjabloon wordt geïnitialiseerd met de huidige datum. Dit zorgt ervoor dat de kalender wordt weergegeven met deze datum als startdatum;
  • regels 223-230: definiëren de gebeurtenishandler die wordt aangeroepen bij het wijzigen van de taal. Let op de syntaxis:
$scope.nom_fonction=function(param1, param2, ...){...}

om een gebeurtenishandler te definiëren die [nom_fonction] zou heten en de parameters [param1, param2, ...] zou accepteren;

Laten we de code HTML uit de vervolgkeuzelijst nog eens bekijken:


    <!-- de talen -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
</div>
  • regel 8: bij selectie van het Frans wordt [setLang('fr')] aangeroepen;
  • regel 9: bij selectie van het Engels wordt [setLang('en')] aangeroepen;
  • regel 3: het attribuut [is-open] is een booleaanse waarde die bepaalt of de vervolgkeuzelijst geopend (true) of gesloten (false) is. Het wordt geïnitialiseerd met de variabele [isopen] uit het view-model;

Laten we teruggaan naar de code van [rdvmedecins.js]:

  • regel 225: we wijzigen de waarde van de variabele [$locale] met de juiste waarde uit het woordenboek [locales];
  • regel 227: we hebben gezegd dat wanneer het model M van een weergave V verandert, de weergave V automatisch wordt vernieuwd met het nieuwe model. Op regel 225 is de waarde van de variabele [$locale] gewijzigd, die geen deel uitmaakt van het model M dat door de weergave V wordt weergegeven. Er moet een manier worden gevonden om dit model M te wijzigen, zodat de kalender wordt vernieuwd en de nieuwe locale gebruikt. Hier wijzigen we de variabele [jour] van het kalendermodel. We initialiseren deze met een nieuwe pointer (new) die verwijst naar een datum die identiek is aan de weergegeven datum. [$scope.jour.getTime()] is het aantal milliseconden dat is verstreken tussen 1 januari 1970 en de door de kalender weergegeven datum. Met dit getal reconstrueren we een nieuwe datum. We krijgen natuurlijk dezelfde datum te zien en de kalender blijft op de datum staan die hij al weergeeft. Maar de waarde van [$scope.jour], die in feite een pointer is, is wel veranderd en de kalender wordt vernieuwd;
  • regel 229: we stellen de waarde van de variabele [isopen] uit het sjabloon in op false. Deze variabele regelt een van de attributen van de vervolgkeuzelijst:

<div class="btn-group" dropdown is-open="isopen">
    <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
    </button>
...
</div>

In regel 1 hierboven verandert het attribuut [is-open] in false, waardoor de keuzelijst wordt gesloten.

3.7.3. Voorbeeld 3: internationalisering van teksten

Laten we terugkomen op de lokalisatie van de kalender:

In [3] zien we dat de kalender in het Engels is, maar de teksten in [Calendrier, Langues] niet. Standaard biedt Angular geen tool voor de internationalisering van berichten. We gaan hier de bibliotheek [angular-translate] (https://github.com/angular-translate/angular-translate) gebruiken.

We gaan het volgende voorbeeld ontwikkelen:

  • in [1], de weergave in het Frans;
  • in [2], de weergave in het Engels;

Laten we eens kijken naar de configuratie die nodig is voor de internationalisering. Het script [rdvmedecins.js] wordt als volgt aangepast:


  // --------------------- Angular-module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// i18n-configuratie
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // Franse berichten
    $translateProvider.translations("fr", {
      'msg_header': 'Artsenpraktijk<br/>Les Médecins Associés',
      'msg_langues': 'Talen',
      'msg_agenda': 'Agenda van {{titel}} {{voornaam}} {{achternaam}}<br/>op {{dag}}',
      'msg_calendrier': 'Kalender',
      'msg_jour': 'Geselecteerde dag: ',
      'msg_meteo': "Vandaag gaat het regenen..."
    });
    // Engelse berichten
    $translateProvider.translations("en", {
      'msg_header': 'The Associated Doctors',
      'msg_langues': 'Talen',
      'msg_agenda': "{{titel}} {{voornaam}} {{achternaam}}'s dagboek<br/> op {{dag}}",
      'msg_calendrier': 'Kalender',
      'msg_jour': 'Geselecteerde dag: ',
      'msg_meteo': 'Vandaag gaat het regenen...'
    });
    // standaardtaal
    $translateProvider.preferredLanguage("fr");
}]);
  • regel 2: de eerste wijziging is het toevoegen van een nieuwe afhankelijkheid. Voor de internationalisering van de applicatie is de Angular-module [pascalprecht.translate] nodig;
  • regels 5-26: hierin wordt de functie [config] van de module [rdvmedecins] gedefinieerd. Bij het opstarten van een Angular-applicatie maakt het framework instanties aan van alle services die de applicatie nodig heeft, zowel de vooraf gedefinieerde services van Angular als de door de gebruiker gedefinieerde services. Tot nu toe hebben we nog geen services gedefinieerd. De functie [config] van de module van een applicatie wordt uitgevoerd vóór het instantiëren van welke service dan ook. Deze kan worden gebruikt om configuratie-informatie te definiëren voor de services die vervolgens zullen worden geïnstantieerd. Hier wordt de functie [config] gebruikt om de geïnternationaliseerde berichten van de applicatie te definiëren;
  • regel 5: de parameter van de functie [config] is een array ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)], waarbij Oi een bekend object is dat door Angular wordt geleverd. Hier wordt het object [$translateProvider] geleverd door de module [pascalprecht.translate]. [function] is de functie die wordt uitgevoerd om de applicatie te configureren;
  • regels 7-14: de functie [$translateProvider.translations] heeft twee parameters:
    • de eerste parameter is de sleutel van een taal. Je kunt hierin invoeren wat je wilt. Hier hebben we 'fr' ingevoerd voor de Franse vertalingen (regel 7) en 'en' voor de Engelse vertalingen (regel 16),
    • de tweede is de lijst met vertalingen in de vorm van een woordenboek {'sleutel1':'msg1', 'sleutel2':'msg2', ...};
  • regels 7-14: de Franse berichten;
  • regels 16-23: de Engelse berichten;
  • regel 25: de methode [preferredLanguage] stelt de standaardtaal in. De parameter hiervan is een van de argumenten die als eerste parameter van de functie [$translateProvider.translations] worden gebruikt, dus hier ofwel 'fr' (regel 7), ofwel 'en' (regel 16);
  • merk op dat er drie soorten berichten zijn:
    • berichten zonder parameters of HTML-elementen (regels 9, 11, 12, ...),
    • berichten met HTML-elementen (regels 8, 10, ...),
    • berichten met parameters (regels 10, 19);

We kopiëren nu [app-11.html] naar [app-12.html] en brengen de volgende wijzigingen aan:


<div class="container">
  <!-- een eerste tekst met daarin HTML-elementen -->
  <h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
  <!-- een tweede tekst met parameters -->
  <h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
  <!-- een derde tekst, vertaald door de controller -->
  <h3 class="alert alert-danger">{{msg2}}</h3>

  <pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
  <div class="row">
    <!-- de kalender-->
    <div class="col-md-4">
      <h4>{{'msg_calendrier'|translate}}</h4>

      <div style="display:inline-block; min-height:290px;">
        <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
      </div>
    </div>
    <!-- de talen -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          {{'msg_langues'|translate}}<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>
  • de vertalingen vinden plaats op de regels 3, 5, 9, 13, 23;
  • er zijn drie syntaxisvormen te onderscheiden:
    • de syntaxis [translate={{'msg_key'}}] (regel 3), waarbij [msg_key] een van de sleutels is van een vertaalwoordenboek. Deze syntaxis is geschikt voor berichten met of zonder HTML-elementen, maar niet voor berichten met parameters;
    • de syntaxis [translate={{'msg_key'}} translate-values={{dictionnaire]}}] (regel 5), is geschikt voor berichten met of zonder HTML-elementen en met parameters;
    • de syntaxis [{{'msg_key'|translate}}] (regels 9, 13, 23) is geschikt voor berichten zonder parameters en zonder HTML-elementen;

Laten we de verschillende berichten in dit overzicht eens bekijken:

ligne
français
anglais
3
Artsenpraktijk<br/>Les Médecins Associés
The Associated Doctors
13
Agenda
Kalender
23
Talen
Languages
9
Geselecteerde dag:
Geselecteerde dag:

Laten we nu eens kijken naar regel 5:


<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

Merk op dat [msg.text] en [msg.model] niet tussen aanhalingstekens staan. Dit zijn geen tekenreeksen, maar elementen van het sjabloon:

  • msg.text: definieert de sleutel van het te gebruiken geparametriseerde bericht;
  • msg.model: is het woordenboek dat de waarden van de parameters levert;

De veldnamen [text, model] kunnen willekeurig zijn. In de controller [rdvMedecinsCtrl] van de weergave wordt het object [msg] als volgt gedefinieerd:

Image

  • regel 245: de definitie van het object [msg];
  • regel 245: het veld [text] heeft als waarde de sleutel [msg_agenda], die gekoppeld is aan twee waarden:
    • Agenda van {{titel}} {{voornaam}} {{achternaam}}<br/>op {{dag}} in het Franse woordenboek;
    • {{titel}} {{voornaam}} {{achternaam}}'s dagboek<br/> op {{dag}} in het Engelse woordenboek;

Het weer te geven bericht heeft dus vier parameters: [titre, prenom, nom, jour];

  • regel 245: het veld [model] is een woordenboek dat een waarde toekent aan deze vier parameters. Er is een probleem met de parameter [jour]. We willen de volledige naam van de dag weergeven. Deze verschilt naargelang het Frans of Engels is. We gebruiken daarom het filter [date] dat al in de weergave wordt gebruikt in de vorm {{ dag | date:'fullDate'}}. Het is mogelijk om elk filter in de JavaScript-code te gebruiken in de vorm $filter('filter')(waarde, aanvullingen), waarbij $filter een vooraf gedefinieerd Angular-object is en 'filter' de naam van het filter;
  • regels 33-34: het vooraf gedefinieerde object $filter wordt als parameter doorgegeven aan de controller, waardoor het op regel 245 kan worden gebruikt;

Laten we teruggaan naar een andere regel van de weergegeven weergave:


  <!-- een derde tekst, vertaald door de controller -->
<h3 class="alert alert-danger">{{msg2}}</h3>

Alle voorgaande vertalingen zijn in de weergave uitgevoerd met behulp van attributen van de module [pascalprecht.translate]. Men kan er ook voor kiezen om deze vertaling aan de serverzijde uit te voeren. Dat is hier het geval. In de controller (regel 247 in de bovenstaande schermafbeelding) staat de volgende code:


$scope.msg2 = $filter('translate')('msg_meteo');

We gebruiken dezelfde syntaxis als voor het filter 'date', omdat 'translate' ook een filter is. We vragen hier om het sleutelbericht 'msg_meteo'.

Laten we eens kijken naar het mechanisme achter de taalwisselingen. We hebben gezien dat de configuratiefunctie [config] van de module [rdvmedecins] het Frans als standaardtaal had aangewezen (regel 9 hieronder):


// i18n-configuratie
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // Franse berichten
    $translateProvider.translations("fr", {...});
    // Engelse berichten
    $translateProvider.translations("en", {...});
    // standaardtaal
    $translateProvider.preferredLanguage("fr");
}]);

We herinneren er ook aan dat de standaardlocale eveneens Frans was. Bij de initialisatie van de controller [rdvmedecins] is het volgende geschreven:


// de locale wordt ingesteld op Frans
angular.copy(locales['fr'], $locale);
  • regel 2: [locales] is een woordenboek dat we hebben samengesteld;

Er is geen verband tussen de internationalisering van berichten door de module [pascalprecht.translate] en de lokalisatie van datums die wij hebben geïmplementeerd. Deze laatste maakt gebruik van een variabele $locale die niet wordt gebruikt door de module [pascalprecht.translate]. Het zijn twee processen die elkaar niet beïnvloeden.

Het is nu tijd om te kijken wat er gebeurt wanneer de gebruiker van taal wisselt:

Image

  • regel 251: bij een taalwisseling wordt de functie [setLang] aangeroepen met een van de twee parameters ['fr','en'];
  • regels 252-257: zijn al uitgelegd – ze wijzigen de variabele [$locale] van de kalender. Dit heeft geen invloed op de taal van de vertalingen;
  • regel 259: de taal van de vertalingen wordt gewijzigd. Hiervoor wordt het object [$translate] gebruikt, dat wordt geleverd door de module [pascalprecht.translate]. Hiervoor moet het in de controller worden geïnjecteerd:

// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {

In de regels 3 en 4 hierboven wordt het object $translate ingevoegd;

  • de parameter lang van de functie [$translate.use(lang)] moet een waarde hebben die overeenkomt met een van de sleutels die in de configuratie worden gebruikt als eerste parameter van de functie [$translateProvider.translations], d.w.z. ofwel 'fr', ofwel 'en'. Dit is inderdaad het geval;
  • regel 261: de waarde van msg2 wordt opnieuw berekend. Waarom? In de weergave worden, na de taalwijziging die door regel 259 wordt doorgevoerd, alle aanwezige [translate]-attributen opnieuw geëvalueerd. Dit geldt niet voor de uitdrukking {{msg2}}, die dit attribuut niet heeft. Daarom wordt de nieuwe waarde ervan in de controller berekend. Dit moet gebeuren na de taalwijziging in regel 259, zodat de nieuwe taal wordt gebruikt voor de berekening van [msg2];

Als we het hierbij laten, zien we twee afwijkingen:

  1. in [1] is de dag in het Frans gebleven, terwijl de rest van de weergave in het Engels is;
  2. in [2] en [3] is de geselecteerde dag 24 juni, terwijl in [1] de dag op 20 juni blijft staan;

Laten we eerst proberen dit te verklaren voordat we oplossingen zoeken. Het bericht [1] wordt in de controller opgebouwd met de volgende code:


      $scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};

en wordt in de weergave weergegeven met de volgende code:


  <h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

De afwijking [1] (de dag is in het Frans gebleven terwijl de rest van de weergave in het Engels is) lijkt aan te tonen dat, hoewel het attribuut [translate] bij een taalwisseling opnieuw wordt geëvalueerd, het attribuut [translate-values] niet opnieuw is geëvalueerd. We kunnen deze evaluatie dan in de controller forceren:


      // ------------------- gebeurtenisbeheerder
      // taalwijziging
      $scope.setLang = function (lang) {
...
        // msg2 wordt bijgewerkt
        $scope.msg2 = $filter('translate')('msg_meteo');
        // en de dag van het bericht
        $scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};

Bij elke taalwisseling wordt in regel 8 hierboven de weergegeven dag opnieuw berekend. Dit lost het eerste probleem wel op, maar niet het tweede (de dag die in het bericht wordt weergegeven, verandert niet wanneer je een andere dag in de kalender selecteert). De reden voor dit gedrag is als volgt. Het bericht wordt in de weergave getoond met de volgende code:


<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

De weergegeven weergave V verandert alleen als het bijbehorende sjabloon M verandert. Maar in dit geval leidt het selecteren van een nieuwe dag in de kalender tot een gebeurtenis die niet wordt afgehandeld, waardoor het sjabloon [msg] niet verandert en de weergave dus ook niet verandert. We passen de definitie van de kalender in de weergave aan:


<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>

Hierboven geven we aan dat een klik op de kalender moet worden afgehandeld door de functie [$scope.calendarClick]. Deze functie is als volgt:

Image

  • regel 267: de handler voor de klik op de kalender;
  • regel 269: we dwingen de weergave van de dag bij te werken met het bericht [msg];

3.7.4. Voorbeeld 4: een configuratieservice

Laten we terugkeren naar de architectuur van een Angular-applicatie JS:

We gaan hier in op het begrip ‘service’. Dit is een vrij breed begrip. Hoewel de laag [DAO] hierboven duidelijk een service is, kan elk Angular-object een service worden:

  • een service volgt een specifieke syntaxis. Het heeft een naam en Angular herkent het aan de hand van die naam;
  • een service kan door Angular worden geïnjecteerd in controllers en andere services;

Sommige van de services die we in de module [rdvmedecins] gaan configureren, moeten worden ingesteld. Aangezien een service in een andere service kan worden geïnjecteerd, is het verleidelijk om de configuratie uit te voeren in een service die we [config] zullen noemen en deze vervolgens te injecteren in de te configureren services en controllers. We beschrijven dit proces nu.

We dupliceren [app-13.html] naar [app-14.html] en brengen de volgende wijzigingen aan:


<div class="container">
  <!-- controle van het wachtbericht -->
  <label>
    <input type="checkbox" ng-model="waiting.visible">
    <span>Voir le message d'attente</span>
  </label>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
            {{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
...
</div>
...
<script type="text/javascript" src="rdvmedecins-02.js"></script>
  • regels 3-6: een selectievakje dat bepaalt of het wachtbericht van regels 9-15 al dan niet wordt weergegeven. De waarde van het selectievakje wordt opgeslagen in de variabele [waiting.visible] van het M-model van de V-weergave. Deze waarde is true als het selectievakje is aangevinkt en false anders. Dit werkt in beide richtingen. Als we de variabele [waiting.visible] de waarde true toekennen, wordt het selectievakje aangevinkt. Er is een bidirectionele koppeling tussen de weergave V en het bijbehorende model M;
  • regel 9-15: een wachtmelding met een knop om het wachten te annuleren (regel 11);
  • regel 9: het bericht is alleen zichtbaar als de variabele [waiting.visible] de waarde ‘true’ heeft. Dus wanneer we het selectievakje op regel 4 aanvinken:
    • wordt de waarde true toegewezen aan de variabele [waiting.visible] (ng-model, regel 4);
    • aangezien er een wijziging in het model M heeft plaatsgevonden, wordt de weergave V automatisch opnieuw geëvalueerd. Het wachtbericht wordt dan zichtbaar gemaakt (ng-show, regel 9);
    • de redenering is analoog wanneer het selectievakje op regel 4 wordt uitgeschakeld: het wachtbericht wordt verborgen;
  • regel 10: het wachtbericht wordt vertaald (filter translate);
  • regel 11: wanneer op de knop wordt geklikt, wordt de methode [waiting.cancel()] uitgevoerd (attribuut ng-click);
  • regel 12: de tekst van de knop wordt vertaald;
  • regel 19: de JavaScript-code van de applicatie wordt in een nieuw bestand JS [rdvmedecins-02] geplaatst om de reeds geschreven code, die nu moet worden gereorganiseerd, niet kwijt te raken;

Dit levert het volgende resultaat op:

  • in [1], vakje niet aangevinkt;
  • in [2], vakje aangevinkt;

Het script [rdvmedecins-02] is een herschikking van het script [rdvmedecins]:

Image

  • regel 6: de module [rdvmedecins] van de applicatie;
  • regels 9-10: de configuratiefunctie van de applicatie;
  • regels 38-39: de service [config];
  • regels 283-284: de controller [rdvMedecinsCtrl];

Eerder hadden we in de controller het woordenboek locales={'fr':..., 'en': ...} gedefinieerd, dat 200 regels telde. Dit woordenboek is duidelijk een configuratie-element, dus verplaatsen we het naar de service [config] in de regels 38-39. Deze service is als volgt gedefinieerd:

Image

  • regels 38-39: er wordt een service aangemaakt met de functie [factory] van het object [angular.module]. De syntaxis van deze functie is dezelfde als bij de voorgaande: factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){...}]), waarbij de Oi's de namen zijn van objecten die Angular kent (vooraf gedefinieerd of door de ontwikkelaar aangemaakt) en die Angular als parameter in de factory-functie injecteert. Aangezien de functie hier geen parameters heeft, is een kortere, eveneens toegestane syntaxis gebruikt: `factory('nom_service', function (){...})]`;
  • regel 40: de functie [factory] moet de service implementeren door middel van een object dat zij retourneert. Dit object is de service. Daarom wordt de functie ‘factory’ (objectfabriek) genoemd;

Over het algemeen heeft de code van een service de volgende vorm:


Angular.module('nom_module')
  .factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
     // voorbereiding van de service
    ...
     // het object dat de service implementeert wordt teruggegeven
    return {
         // velden
        ...
         // methoden
        ...
        }
});
  • regel 6: er wordt een object JS geretourneerd dat zowel velden als methoden kan bevatten. Het zijn deze methoden die de service uitvoeren;

Hier definieert de service [config] alleen velden en geen methoden. Hierin wordt alles opgenomen wat in de applicatie kan worden geconfigureerd:

  • regels 42-47: de sleutels van de te vertalen berichten;
  • regels 59-62: de URL van de applicatie;
  • regels 64-69: de URL-codes van de externe webservice;
  • regel 71: een HTTP-aanroep naar een webservice die niet reageert, kan lang duren. Hier wordt de maximale wachttijd voor het antwoord van de webservice ingesteld op 1 seconde. Na deze tijd mislukt de aanroep HTTP en wordt een uitzondering JS gegenereerd;
  • regel 73: vóór elke aanroep naar de server wordt een wachttijd gesimuleerd, waarvan de duur hier in milliseconden wordt vastgesteld. Een wachttijd van 0 betekent dat er niet wordt gewacht. De applicatie wordt zo ontworpen dat de gebruiker een door hem gestarte bewerking kan annuleren. Om te kunnen worden geannuleerd, moet de bewerking minstens enkele seconden duren. We gebruiken deze kunstmatige wachttijd om langdurige bewerkingen te simuleren;
  • regel 75: in de modus [debug=true] wordt aanvullende informatie weergegeven in de huidige weergave. Standaard is deze modus ingeschakeld. In de productieomgeving zouden we dit veld instellen op false;
  • regels 77-278: het woordenboek voor de twee landinstellingen 'fr' en 'en'. Dit stond voorheen in de controller [rdvMedecinsCtrl];

Met deze service verandert de controller [rdvMedecinsCtrl] als volgt:

Image

  • regels 284-285: de service [config] wordt in de controller geïnjecteerd;
  • regel 290: het woordenboek [locales] bevindt zich nu in de service [config] en niet langer in de controller;
  • regel 294: het object [waiting] dat de weergave van het wachtbericht regelt. De sleutel van het wachtbericht is te vinden in de service [config] (veld text). Standaard is het wachtbericht verborgen (veld visible). Het veld cancel heeft als waarde de naam van de functie op regel 316. Dit veld is dus een methode of functie;
  • regel 316: de functie [cancel] is privé (we hebben niet geschreven: $scope.cancel=function(){}). Laten we teruggaan naar de code van de annuleerknop:

<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">

Wanneer de gebruiker op de annuleerknop klikt, wordt de methode [$scope.waiting.cancel()] aangeroepen. Uiteindelijk wordt de privé-functie cancel op regel 316 uitgevoerd. Deze functie verbergt het wachtbericht door de variabele van het model [waiting.visible] (regel 318) op false te zetten;

3.7.5. Voorbeeld 5: asynchrone programmering

We introduceren nu een nieuwe service met een nieuw concept: asynchroon programmeren.

Onze applicatie zal drie services hebben:

  • [config]: de configuratieservice die we zojuist hebben besproken;
  • [utils]: een service met hulpprogramma's. We zullen er twee presenteren;
  • [dao]: de service voor toegang tot de webservice voor het maken van afspraken. Deze zullen we binnenkort bespreken;

We gaan de volgende applicatie schrijven:

  • het doel is om de banner [2] weer te geven gedurende een tijd die wordt bepaald door [1]. Het wachten kan worden geannuleerd door [3].

We kopiëren [app-01.html] naar [app-15.html] en passen de code als volgt aan:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  <title>RdvMedecins</title>
  ...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>

  <!-- het formulier -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="form-group">
      <label for="waitingTime">{{waitingTimeText | translate}}</label>
      <input type="text" id="waitingTime" ng-model="waiting.time"/>
    </div>
    <button class="btn btn-primary" ng-click="execute()">Exécuter</button>
  </div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
</html>
  • regel 11: het attribuut [ng-cloak] voorkomt dat het veld wordt weergegeven voordat de bijbehorende Angular-expressies zijn berekend. Dit voorkomt dat het veld kortstondig wordt weergegeven voordat het attribuut [ng-show] is geëvalueerd, wat er namelijk voor zorgt dat het veld wordt verborgen;
  • regel 22: de invoer van de gebruiker (wachttijd) wordt opgeslagen in het model [waiting.time] (attribuut ng-model);
  • regel 28: de pagina maakt gebruik van een nieuw script [rdvmedecins-03];

Het script [rdvmedecins-03] is als volgt:

Image

  • regel 6: de Angular-module die de applicatie beheert;
  • regel 10: de functie [config] die wordt gebruikt om berichten te internationaliseren;
  • regel 41: de service [config] die we hebben beschreven;
  • regel 286: de service [utils] die we gaan bouwen;
  • regel 315: de controller [rdvmedecinsCtrl] die we gaan bouwen;

We voegen aan de functie [config] een nieuwe berichtcode toe (regels 6, 11):


angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // Franse berichten
    $translateProvider.translations("fr", {
...
      'msg_waiting_time_text': "Wachttijd: "
    });
    // Engelse berichten
    $translateProvider.translations("en", {
...
      'msg_waiting_time_text': "Wachttijd:"
    });
    // standaardtaal
    $translateProvider.preferredLanguage("fr");
}]);

We voegen aan de service [config] een nieuwe regel (regel 6) toe voor deze berichtcode:


angular.module("rdvmedecins")
  .factory('config', function () {
    return {
      // te internationaliseren berichten
      ...
waitingTimeText: 'msg_waiting_time_text',

De service [utils] bevat twee methoden (regels 4, 12):


angular.module("rdvmedecins")
  .factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
    // weergave van de JSON-representatie van een object
    function debug(message, data) {
      if (config.debug) {
        var text = data ? message + " : " + angular.toJson(data) : message;
        console.log(text);
      }
    }

    // wachten
    function waitForSomeTime(milliseconds) {
      // asynchroon wachten in milliseconden
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // de taak wordt teruggegeven
      return task;
    };

    // instantie van de service
    return {
      debug: debug,
      waitForSomeTime: waitForSomeTime
    }
}]);
  • regel 2: de service heet [utils] (1e parameter). Deze is afhankelijk van drie services: twee vooraf gedefinieerde Angular-services, $timeout en $q, en de service config. Met de service [$timeout] kan een functie worden uitgevoerd nadat er een bepaalde tijd is verstreken. Met de service [$q] kunnen asynchrone taken worden aangemaakt;
  • regel 4: een lokale functie [debug];
  • regel 12: een lokale functie [waitForSomeTime];
  • regels 23-26: de instantie van de service [utils]. Dit is een object dat twee methoden beschikbaar stelt, namelijk die van regel 4 en regel 12. Merk op dat de velden van het object willekeurige namen kunnen hebben. Omwille van de consistentie hebben we ze de namen gegeven van de functies waarnaar ze verwijzen;
  • regels 4-9: de methode [debug] schrijft een bericht [message] naar de console en eventueel de weergave JSON van een object [data]. Hierdoor kunnen objecten van elke complexiteit worden weergegeven;
  • regels 12-20: de methode [waitForSomeTime] maakt een asynchrone taak aan die [milliseconds] milliseconden duurt;
  • regel 14: aanmaken van een taak met behulp van het vooraf gedefinieerde object [$q] (https://docs.angularjs.org/api/ng/service/$q). Hieronder staat de API van de taak die in de Angular-documentatie [deferred] wordt genoemd:

Image

  • een asynchrone taak [task] wordt aangemaakt door de instructie [$q.defer()];
  • deze wordt voltooid met behulp van een van de twee methoden:
    • [task.resolve(value)]: hiermee wordt de taak succesvol beëindigd en wordt de waarde [value] teruggestuurd naar degenen die wachten op de voltooiing van de taak;
    • [task.reject(value)]: hiermee wordt de taak zonder succes beëindigd en wordt de waarde [value] teruggestuurd naar degenen die wachten tot de taak is voltooid;

De taak [task] kan regelmatig informatie verstrekken aan degenen die wachten tot de taak is voltooid:

    • [task.notify(value)]: stuurt de waarde [value] naar degenen die wachten tot de taak is voltooid. De taak blijft worden uitgevoerd;

Degenen die willen wachten tot de taak is voltooid, gebruiken het veld [promise] van de taak:

var promise=[task].promise ;

Het object [promise] heeft de volgende API (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Image

Om zowel het slagen als het mislukken van de taak af te handelen, schrijven we:

1
2
3
var promise=[task].promise;
promise.then(successCallback, errorCallBack);
promise['finally'](finallyCallback);
  • regel 1: we halen de promise van de taak op;
  • regel 2: we definiëren de functies die moeten worden uitgevoerd in geval van succes of in geval van mislukking. We hoeven geen functie voor mislukking op te nemen. De functie [successCallback] wordt pas uitgevoerd aan het einde van de taak [task], bij succes [task.resolve()]. De functie [errorCallBack] wordt pas uitgevoerd na afloop van de taak [task] bij mislukking van [task.reject()].
  • regel 3: hier wordt de functie gedefinieerd die moet worden uitgevoerd nadat een van de twee voorgaande functies is uitgevoerd. Hier wordt de code geplaatst die beide functies gemeen hebben: [successCallback, errorCallBack].

Laten we teruggaan naar de code van de functie [waitForSomeTime]:


    // wachten
    function waitForSomeTime(milliseconds) {
      // asynchroon wachten in milliseconden
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // de taak wordt teruggegeven
      return task;
};
  • regel 4: er wordt een taak aangemaakt;
  • regels 5-7: met het object [$timeout] kan een functie (1e parameter) worden gedefinieerd die wordt uitgevoerd na een bepaalde vertraging, uitgedrukt in milliseconden (2e parameter). Hier is de tweede parameter van de functie [$timeout] de parameter van de methode (regel 1);
  • regel 6: na afloop van de vertraging [milliseconds] is de taak succesvol voltooid;
  • regel 9: de taak [task] wordt teruggegeven. Hierbij moet worden opgemerkt dat regel 9 onmiddellijk na de definitie van het object [$timeout] wordt uitgevoerd. Er wordt niet gewacht tot de tijdlimiet [milliseconds] is verstreken. De code van de regels 2-10 wordt dus op twee verschillende momenten uitgevoerd:
    • een eerste keer bij het definiëren van het object [$timeout];
    • een tweede keer wanneer de time-out [milliseconds] is verstreken;

Dit is een asynchrone functie: het resultaat wordt verkregen op een later tijdstip dan het moment van uitvoering.

De code van de controller die gebruikmaakt van de service [config] is als volgt:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
    function ($scope, utils, config, $filter) {
      // ------------------- initialisatie van het model
      // wachtbericht
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      // wachttaak
      var task;
      // logboeken
      utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
      utils.debug("locales['fr']=", config.locales['fr']);

      // actie uitvoeren
      $scope.execute = function () {
        // logboek
        utils.debug('début', new Date());
        // het wachtbericht wordt weergegeven
        $scope.waiting.visible = true;
        // gesimuleerde wachttijd
        task = utils.waitForSomeTime($scope.waiting.time);
        // einde van de wachttijd
        task.promise.then(function () {
          // geslaagd
          utils.debug('fin', new Date());
        }, function () {
          // mislukt
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          // einde van de wachttijd in alle gevallen
          $scope.waiting.visible = false;
        });

      };

      // wachtrij geannuleerd
      function cancel() {
        // taak wordt voltooid
        task.reject();
      }
    }]);
  • regel 3: de controller maakt gebruik van de service [config];
  • regel 7: het veld [time] is toegevoegd aan het object [$scope.waiting]. Het object [$scope.waiting.time] krijgt de waarde van de door de gebruiker ingestelde wachttijd;
  • regel 8: de sleutel van het wachtbericht dat door de weergave wordt getoond, wordt in het model [$scope.waitingTimeText] geplaatst. In het algemeen moet alles wat door een V-weergave wordt getoond, in het object [$scope] worden geplaatst;
  • regel 10: een lokale variabele. Deze wordt niet blootgesteld aan de weergave V;
  • regels 12-13: gebruik van de methode [debug] van de service [config]. Het volgende resultaat wordt op de console weergegeven:
libellé temps d'attente : "Temps d'attente : "
locales['fr']= : {"DATETIME_FORMATS":{"AMPMS":["AM","PM"],"DAY":["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],"MONTH":["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],"SHORTDAY":["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],"SHORTMONTH":["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],"fullDate":"EEEE d MMMM y","longDate":"d MMMM y","medium":"d MMM y HH:mm:ss","mediumDate":"d MMM y","mediumTime":"HH:mm:ss","short":"dd/MM/yy HH:mm","shortDate":"dd/MM/yy","shortTime":"HH:mm"},"NUMBER_FORMATS":{"CURRENCY_SYM":"","DECIMAL_SEP":",","GROUP_SEP":" ","PATTERNS":[{"gSize":3,"lgSize":3,"macFrac":0,"maxFrac":3,"minFrac":0,"minInt":1,"negPre":"-","negSuf":"","posPre":"","posSuf":""},{"gSize":3,"lgSize":3,"macFrac":0,"maxFrac":2,"minFrac":2,"minInt":1,"negPre":"(","negSuf":" ¤)","posPre":"","posSuf":" ¤"}]},"id":"fr-fr"}

Op regel 2 krijgen we de notatie JSON van het object locales['fr'].

  • regel 16: de methode die wordt uitgevoerd wanneer de gebruiker op de knop [Executer] klikt;
  • regel 18: geeft het tijdstip weer waarop de methode wordt gestart;
  • regel 22: de taak [waitForSomeTime] wordt gestart. Er wordt niet gewacht tot deze is voltooid. De uitvoering gaat verder met de volgende regel 24;
  • regels 24-30: hier worden de functies gedefinieerd die moeten worden uitgevoerd wanneer de taak succesvol is voltooid (regel 26) en in geval van een fout (regel 29);
  • regel 26: geeft het tijdstip weer waarop de methode is voltooid;
  • regel 29: geeft aan dat de bewerking is geannuleerd. Dit gebeurt alleen wanneer de gebruiker op de knop [Annuler] klikt. De instructie op regel 41 stopt vervolgens de asynchrone taak met een foutcode;
  • regels 31-34: hier wordt de functie gedefinieerd die moet worden uitgevoerd nadat een van de twee voorgaande functies is uitgevoerd;

Het is belangrijk om de uitvoeringsvolgorde van deze code te begrijpen. In het geval dat de gebruiker een vertraging van 3 seconden instelt en het wachten niet annuleert:

  • wanneer hij op de knop [Exécuter] klikt, wordt de functie [$scope.execute] uitgevoerd. De regels 16-34 worden uitgevoerd zonder de 3 seconden af te wachten. Aan het einde van deze uitvoering wordt de weergave V gesynchroniseerd met het model M. Het wachtbericht wordt weergegeven (ng-show=$scope.waiting.visible=true, regel 20) en het formulier wordt verborgen (ng-hide=$scope.waiting.visible=true, regel 20);
  • vanaf dat moment kan de gebruiker weer interactie hebben met de weergave. Hij kan met name op de knop [Annuler] klikken;
  • als hij dat niet doet, wordt na 3 seconden de functie van [$timeout] (zie regels 5-7 hieronder) uitgevoerd:

    // wachten
    function waitForSomeTime(milliseconds) {
      // asynchroon wachten van milliseconden milliseconden
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // de taak wordt teruggegeven
      return task;
};
  • na 3 seconden wordt dus code uitgevoerd. Deze code beëindigt de taak [task] met een succescode (resolve). Dit zorgt ervoor dat alle codes die op deze afsluiting wachtten, worden uitgevoerd (regel 4 hieronder):

        // gesimuleerd wachten
        task = utils.waitForSomeTime($scope.waiting.time);
        // einde van het wachten
        task.promise.then(function () {
          // geslaagd
          utils.debug('fin', new Date());
        }, function () {
          // mislukt
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          // einde van de wachttijd in alle gevallen
          $scope.waiting.visible = false;
        });

  • regel 6 hierboven (succesvolle afsluiting) wordt dus uitgevoerd. Vervolgens is het de beurt aan de regels 11-14. Zodra deze code is uitgevoerd, keert men terug naar de weergave V, die vervolgens wordt gesynchroniseerd met het bijbehorende model M. Het wachtbericht wordt verborgen (ng-show=$scope.waiting.visible=false, regel 13) en het formulier wordt weergegeven (ng-hide=$scope.waiting.visible=false, regel 13);

De schermweergaven zijn dan als volgt:

début : "2014-06-23T15:05:58.480Z"
fin : "2014-06-23T15:06:01.481Z"

Hierboven is de vertraging van 3 seconden (06:01-05:58) tussen het begin en het einde van de wachttijd te zien. Als de gebruiker daarentegen de wachttijd vóór het verstrijken van de 3 seconden annuleert, verschijnt de volgende weergave:

début : "2014-06-23T15:08:09.564Z"
Opération annulée

Tot slot is het belangrijk om te begrijpen dat er op elk moment slechts één uitvoeringsthread is, de zogenaamde UI-thread (User Interface). Het einde van een asynchrone taak wordt gemeld door een gebeurtenis, net zoals dat het geval is bij het klikken op een knop. Deze gebeurtenis wordt niet onmiddellijk verwerkt. Het wordt in de wachtrij geplaatst van gebeurtenissen die wachten op uitvoering. Wanneer het aan de beurt is, wordt het verwerkt. Deze verwerking maakt gebruik van de thread van de UI en daarom is de interface gedurende die tijd bevroren. Ze reageert niet op acties van de gebruiker. Daarom is het belangrijk dat de verwerking van een gebeurtenis snel verloopt. Omdat elke gebeurtenis wordt verwerkt door de thread van UI, hoeven er nooit synchronisatieproblemen te worden opgelost tussen threads die tegelijkertijd worden uitgevoerd. Er wordt op elk moment slechts de thread van UI uitgevoerd.

3.7.6. Voorbeeld 6: de HTTP-services

We presenteren nu de service [dao] die communiceert met de webserver:

3.7.6.1. De weergave V

We gaan een formulier schrijven om de lijst met artsen op te vragen:

Image

We dupliceren [app-01.html] naar [app-16.html], dat we vervolgens als volgt aanpassen:


<div class="container" ng-cloak="">
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>

  <!-- de aanvraag -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="form-group">
      <label for="waitingTime">{{waitingTimeText | translate}}</label>
      <input type="text" id="waitingTime" ng-model="waiting.time"/>
    </div>
    <div class="form-group">
      <label for="urlServer">{{urlServerLabel | translate}}</label>
      <input type="text" id="urlServer" ng-model="server.url"/>
    </div>
    <div class="form-group">
      <label for="login">{{loginLabel | translate}}</label>
      <input type="text" id="login" ng-model="server.login"/>
    </div>
    <div class="form-group">
      <label for="password">{{passwordLabel | translate}}</label>
      <input type="password" id="password" ng-model="server.password"/>
    </div>
    <button class="btn btn-primary" ng-click="execute()">{{medecins.title|translate:medecins.model}}</button>
  </div>

  <!-- de lijst met artsen -->
  <div class="alert alert-success" ng-show="medecins.show">
    {{medecins.title|translate:medecins.model}}
    <ul>
      <li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
    </ul>
  </div>

  <!-- de foutenlijst -->
  <div class="alert alert-danger" ng-show="errors.show">
    {{errors.title|translate:errors.model}}
    <ul>
      <li ng-repeat="message in errors.messages">{{message|translate}}</li>
    </ul>
  </div>

</div>
...
<script type="text/javascript" src="rdvmedecins-04.js"></script>
  • regels 13-31: implementeren het formulier. Dit is niet zichtbaar wanneer het wachtbericht wordt weergegeven (ng-hide="waiting.visible"). Let op: de vier invoervelden worden opgeslagen in (ng-model-attributen) [waiting.time (ligne 16), server.url (ligne 20), server.login (ligne 24), server.password (ligne 28)];
  • regels 34-39: geven de lijst met artsen weer. Deze lijst is niet altijd zichtbaar (ng-show="medecins.show").
  • regel 35: een alternatief voor de reeds eerder genoemde syntaxis <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}">;
  • regel 36: een ongeordende lijst;
  • regel 37: de lijst met artsen is te vinden in het model [medecins.data]. Met de Angular-richtlijn [ng-repeat] kun je door een lijst lopen. De syntaxis ng-repeat="medecin in medecins.data" geeft aan dat de tag <li> voor elk element van de lijst [medecins.data] moet worden herhaald. Het huidige element van de lijst wordt [medecin] genoemd;
  • regel 37: voor elke <li> worden de titel, de voornaam en de achternaam van de huidige arts, aangeduid door de variabele [medecin], weergegeven;
  • regels 42-47: geven de lijst met fouten weer. Deze lijst is niet altijd zichtbaar (ng-show="errors.show"). Deze weergave volgt hetzelfde patroon als de weergave van de lijst met artsen. Over het algemeen wordt voor het weergeven van een lijst met objecten de Angular-richtlijn [ng-repeat] gebruikt;
  • regel 51: de JavaScript-code staat nu in het bestand [rdvmedecins-04]

3.7.6.2. De controller C en het model M

De JavaScript-code verandert als volgt:

Image

  • regels 6-9: de module [rdvmedecins] declareert een afhankelijkheid van de module [base64], geleverd door de bibliotheek [angular-base64], die een van de afhankelijkheden van het project is. Deze module wordt gebruikt om de tekenreeks [login:password], die naar de webservice wordt verzonden voor authenticatie, in Base64 te coderen;
  • regels 12-13: de initialisatiefunctie die onze geïnternationaliseerde berichten bevat. Er verschijnen nieuwe berichten. We zullen deze niet meer toelichten;
  • regels 69-70: de service [config] die onze applicatie configureert. Er zijn nieuwe berichtcodes aan toegevoegd. We zullen deze niet meer toelichten;
  • regels 318-319: de service [utils] die hulpprogramma's bevat. Er worden nieuwe aan toegevoegd. Deze zullen we wel bespreken;
  • regels 385-386: de service [dao] die verantwoordelijk is voor de communicatie met de webservice. Hierop gaan we ons concentreren;
  • regels 467-468: de C-controller van de V-view die we zojuist hebben besproken. We gaan deze nu bespreken, omdat deze de regisseur is die reageert op verzoeken van de gebruiker;

3.7.6.3. De C-controller

De code van de controller is als volgt:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- initialisatie van het sjabloon
      // sjabloon
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      $scope.server = {url: undefined, login: undefined, password: undefined};
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      $scope.errors = {show: false, model: {}};
      $scope.urlServerLabel = config.urlServerLabel;
      $scope.loginLabel = config.loginLabel;
      $scope.passwordLabel = config.passwordLabel;

      // asynchrone taak
      var task;

      // actie uitvoeren
      $scope.execute = function () {
        // UI wordt bijgewerkt
        $scope.waiting.visible = true;
        $scope.medecins.show = false;
        $scope.errors.show = false;
        // gesimuleerde wachttijd
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // wachten
        promise = promise.then(function () {
          // de lijst met artsen wordt opgevraagd;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
        });
        // het resultaat van de vorige aanroep wordt geanalyseerd
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // de verzamelde gegevens worden in het model geplaatst
            $scope.medecins.data = result.data;
            // de UI wordt bijgewerkt
            $scope.medecins.show = true;
            $scope.waiting.visible = false;
          } else {
            // er zijn fouten opgetreden bij het ophalen van de lijst met artsen
            $scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // het bestand UI wordt bijgewerkt
            $scope.waiting.visible = false;
          }
        });
      };

      // annulering in afwachting
      function cancel() {
        // de taak wordt voltooid
        task.reject();
        // het bestand UI wordt bijgewerkt
        $scope.waiting.visible = false;
        $scope.medecins.show = false;
        $scope.errors.show = false;
      }

    }
  ])
;
  • regel 2: de controller heeft een nieuwe afhankelijkheid, namelijk die van de service [dao];
  • regels 6-13: het model M van de weergave V wordt geïnitialiseerd voor de eerste weergave ervan;
  • regel 8: [$scope.server] wordt gebruikt om drie van de vier gegevens uit formulier V op te halen; het vierde gegeven wordt opgeslagen in [$scope.waiting.time] (regel 6);
  • regel 9: [$scope.medecins] verzamelt de gegevens die nodig zijn voor de weergave van de lijst met artsen:

  <!-- de lijst met artsen -->
  <div class="alert alert-success"  ng-show="medecins.show">
    {{medecins.title|translate:medecins.model}}
    <ul>
      <li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
    </ul>
</div>

Het attribuut [medecins.title] zal de titel van de banner zijn. Dit wordt gedefinieerd in de service [config]. Het attribuut [medecins.show] bepaalt of de banner al dan niet wordt weergegeven (attribuut ng-show="medecins.show"). Het attribuut [medecins.model] is een leeg woordenboek en zal dat ook blijven. Het dient alleen ter illustratie van het gebruik van de vertaalvariant die in regel 3 wordt gebruikt. Het attribuut [medecins.data], dat de lijst met artsen zal bevatten (regel 5), is nog niet gedefinieerd.

  • regel 10: [$scope.errors] verzamelt de informatie die nodig is voor de weergave van de foutenlijst:

  <!-- de foutenlijst -->
  <div class="alert alert-danger"  ng-show="errors.show">
    {{errors.title|translate:errors.model}}
    <ul>
      <li ng-repeat="message in errors.messages">{{message|translate}}</li>
    </ul>
</div>

Het attribuut [errors.title] zal de titel van de banner zijn. Het wordt gedefinieerd in de service [config]. Het attribuut [errors.show] bepaalt of de banner al dan niet wordt weergegeven (attribuut ng-show="errors.show"). Het attribuut [errors.model] is een leeg woordenboek en zal dat ook blijven. Het dient louter ter illustratie van het gebruik van de vertaalvariant die in regel 3 wordt gebruikt. Het attribuut [errors.messages], dat de lijst met weer te geven foutmeldingen zal bevatten (regel 5), is nog niet gedefinieerd.

  • regel 16: de asynchrone taak. De controller start achtereenvolgens twee asynchrone taken. De verwijzingen naar deze opeenvolgende taken worden in de variabele [task] opgeslagen. Hierdoor kunnen ze worden geannuleerd (regel 55);
  • regel 19: de methode die wordt uitgevoerd wanneer de gebruiker op de knop [Liste des médecins] klikt:

    <button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
  • regels 21-23: de visuele interface wordt bijgewerkt: het wachtbericht wordt weergegeven, al het andere wordt verborgen;
  • regel 25: de asynchrone wachttaken wordt aangemaakt. Er wordt een signaal (taak voltooid) ontvangen na afloop van de tijd die de gebruiker in het formulier heeft ingevoerd;
  • regel 26: de promise van de asynchrone taak wordt opgehaald. Het programma dat de taak start, werkt hiermee. Men moet echter de referentie van de taak zelf hebben om deze te kunnen annuleren (regel 55);
  • regels 28-32: we definiëren de taken die moeten worden uitgevoerd zodra het wachten is voltooid;
  • regel 30: we gebruiken de methode [dao.getData] om een nieuwe asynchrone taak te starten. We geven de benodigde informatie door:
    • de root-ID URL van de webservice [$scope.server.url], bijvoorbeeld [http://localhost:8080];
    • de login [$scope.server.login] om in te loggen, bijvoorbeeld [admin];
    • het wachtwoord [$scope.server.password] om in te loggen, bijvoorbeeld [admin];
    • de URL die de gevraagde dienst [config.urlSvrMedecins] uitvoert, in dit geval [/getAllMedecins]. In totaal zal de volledige URL [http://localhost:8080/getAllMedecins] zijn;

De methode [dao.getData] levert een resultaat op dat twee mogelijke vormen kan aannemen:

  • (vervolg)
    • {err: 0, data: [med1, med2, ...]} waarbij [medi] een object is dat een arts vertegenwoordigt (titel, voornaam, achternaam),
    • {err: n, messages: [msg1, msg2, ...]} waarbij [msgi] een foutmelding is en n ongelijk is aan 0;
  • regel 31: de belofte van de taak wordt teruggegeven. Hier is iets dat we moeten begrijpen. We hebben twee beloften:
    • promise.then(): retourneert een eerste belofte [promise1];
    • return task.promise: retourneert een tweede belofte [promise2];
    • uiteindelijk is promise=promise.then(...;return task.promise) een keten van twee beloften [promise2.promise1]. [promise1] wordt pas geëvalueerd wanneer de belofte [promise2] is verkregen, d.w.z. wanneer de taak [dao.getData] is voltooid. De belofte [promise1] is niet afhankelijk van een asynchrone taak. Deze zal dus onmiddellijk worden verkregen;
  • regels 34-50: uit de voorgaande uitleg volgt dat deze regels pas zullen worden uitgevoerd wanneer de taak [dao.getData] is voltooid. De parameter [result] die in regel 34 aan de functie wordt doorgegeven, wordt samengesteld door de methode [dao.getData] en doorgegeven aan de aanroepende code via de bewerking [task.resolve(result)], waarbij [result] de volgende vorm heeft:
    • {err: 0, data: [med1, med2, ...]}, waarbij [medi] een object is dat een arts vertegenwoordigt (titel, voornaam, achternaam),
    • {err: n, messages: [msg1, msg2, ...]} waarbij [msgi] een foutmelding is en n ongelijk is aan 0;
  • regel 37: we kijken naar de foutcode [result.err];
  • regels 38-42: als er geen fout is (result.err == 0), haal je de lijst met artsen op en geef je deze weer;
  • regels 44-47: als er daarentegen wel een fout is (result.err != 0), dan halen we de lijst met foutmeldingen op en geven we deze weer;
  • regels 53-56: het wachtvenster met de annuleerknop blijft zichtbaar zolang de twee asynchrone bewerkingen niet zijn voltooid. Laten we eens kijken wat er gebeurt, afhankelijk van het moment van annuleren:
    • allereerst moet duidelijk zijn dat de regels 19-50 in één keer worden uitgevoerd. Er is dan één asynchrone taak gestart, namelijk die van regel 25;
    • na deze eerste uitvoering wordt de weergave V bijgewerkt en zijn de wachtbalk en de annuleerknop dus zichtbaar. Als de gebruiker het wachten annuleert voordat de taak in regel 25 is voltooid, wordt de methode in regel 53 uitgevoerd en wordt de taak met een fout geannuleerd (regel 55);
    • regels 56-59: de interface wordt bijgewerkt: het formulier wordt opnieuw weergegeven en al het andere wordt verborgen,
    • daarna keert men terug naar weergave V en verwerkt de browser de volgende gebeurtenis. Aangezien de taak is voltooid, wordt de belofte van deze taak opgehaald, wat een gebeurtenis genereert. Deze wordt vervolgens verwerkt;
    • vervolgens worden de regels 28-32 uitgevoerd. Er is geen functie gedefinieerd voor het geval van mislukking, dus er wordt geen code uitgevoerd. Er wordt een nieuwe belofte verkregen, die nog steeds wordt geretourneerd door [promise.then] en nog steeds wordt verkregen,
    • nadat de gebeurtenis is verwerkt, keert men terug naar weergave V en gaat de browser de volgende gebeurtenis verwerken. Aangezien de [promise] uit regel 28 is verwerkt, wordt die uit regel 34 opgelost, wat een nieuwe gebeurtenis veroorzaakt. Deze wordt vervolgens verwerkt;
    • de regels 34-49 worden vervolgens op hun beurt uitgevoerd, omdat de belofte die in regel 34 wordt gebruikt, is vervuld. Opnieuw wordt er geen code uitgevoerd, omdat er geen functie is gedefinieerd voor het geval van mislukking,
    • waardoor we bij regel 50 terechtkomen. Er is geen wachttijd voor een taak meer en de nieuwe weergave V wordt getoond;
    • laten we nu aannemen dat de annulering plaatsvindt terwijl de tweede asynchrone taak [dao.getData] wordt uitgevoerd. De eerdere redenering geldt opnieuw. Het einde van de taak zal ervoor zorgen dat de regels 34-50 worden uitgevoerd met een mislukte taakafsluiting. We zullen straks zien dat de methode [dao.getData] een asynchrone aanroep HTTP naar de webservice uitvoert. Deze aanroep wordt niet geannuleerd, maar het resultaat ervan wordt niet verwerkt.

Het is belangrijk om dit voortdurende heen en weer gaan tussen de weergave van de V-weergave en de verwerking van browsergebeurtenissen te begrijpen. Gebeurtenissen worden veroorzaakt door de gebruiker (een klik) of door systeemoperaties, zoals het einde van een asynchrone bewerking. De rusttoestand van de browser is de weergave van de V-view. De browser wordt uit deze rusttoestand gehaald door een gebeurtenis die plaatsvindt en die hij vervolgens verwerkt. Zodra de gebeurtenis is verwerkt, keert hij terug naar zijn rusttoestand. De V-view wordt dan bijgewerkt als de verwerkte gebeurtenis het M-model ervan heeft gewijzigd. De browser wordt uit zijn rusttoestand gehaald door de volgende gebeurtenis.

Alles vindt plaats in één enkele thread. Twee gebeurtenissen worden nooit gelijktijdig verwerkt. Ze worden sequentieel uitgevoerd. De browser gaat pas over naar de volgende gebeurtenis wanneer de vorige het veld vrijgeeft, meestal omdat deze volledig is verwerkt.

Er is nog één punt dat we moeten uitleggen. Om foutmeldingen weer te geven, schrijven we:


$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};

De lijst met berichten wordt geleverd door de methode [utils.getErrors], gedefinieerd in de service [utils]. Deze methode ziet er als volgt uit:


// analyse van fouten in het antwoord van de server JSON
    function getErrors(data) {
      // gegevens {err:n, messages:[]}, err!=0
      // fouten
      var errors = [];
      // foutcode
      var err = data.err;
      switch (err) {
        case 2 :
          // geen toestemming
          errors.push('not_authorized');
          break;
        case 3 :
          // verboden
          errors.push('forbidden');
          break;
        case 4 :
          // lokale fout
          errors.push('not_http_error');
          break;
        case 6 :
          // document niet gevonden
          errors.push('not_found');
          break;
        default :
          // andere gevallen
          errors = data.messages;
          break;

      }
      // als er geen bericht is, wordt er een weergegeven
      if (! errors || errors.length == 0) {
        errors=['error_unknown'];
      }
      // de lijst met fouten weergeven
      return errors;
    }
  • regels 2-3: de ontvangen parameter [data] is een object met twee attributen:
    • [err]: een foutcode;
    • [messages]: een lijst met berichten;
  • regel 5: we gaan een array met foutmeldingen samenstellen. Deze meldingen zijn geïnternationaliseerd. Om deze reden worden niet de berichten zelf in de array geplaatst, maar hun internationalisatiesleutels, behalve op regel 27. In dit geval wordt het attribuut [messages] van de parameter [data] gebruikt. Deze berichten zijn echte berichten en geen berichtensleutels. De weergave V zal ze echter behandelen als berichtcodes die dan niet worden gevonden. In dit geval geeft de module [translate] de berichtcode weer die hij niet heeft gevonden, dus hier een echt bericht. Dit is het gewenste resultaat;
  • regels 32-34: behandelen het geval waarin [data.messages] op regel 27 gelijk is aan null. Dit gebeurt bij de geschreven webservice. Dit geval had moeten worden voorkomen.

3.7.6.4. De service [dao]

De service [dao] zorgt voor de uitwisselingen HTTP met de webservice / JSON. De code ervan is als volgt:


angular.module("rdvmedecins")
  .factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
    function ($http, $q, config, $base64, utils) {

      // logbestanden
      utils.debug("[dao] init");

      // ----------------------------------privé-methoden
      // gegevens ophalen bij de webservice
      function getData(serverUrl, username, password, urlAction, info) {
        // asynchrone bewerking
        var task = $q.defer();
        // URL-verzoek HTTP
        var url = serverUrl + urlAction;
        // basisauthenticatie
        var basic = "Basic " + $base64.encode(username + ":" + password);
        // het antwoord
        var réponse;
        // alle HTTP-verzoeken moeten worden geauthenticeerd
        var headers = $http.defaults.headers.common;
        headers.Authorization = basic;
        // we voeren het verzoek HTTP uit
        var promise;
        if (info) {
          promise = $http.post(url, info, {timeout: config.timeout});
        } else {
          promise = $http.get(url, {timeout: config.timeout});
        }
        promise.then(success, failure);
        // we sturen de taak zelf terug zodat deze kan worden geannuleerd
        return task;

        // geslaagd
        function success(response) {
          // response.data={status:0, data:[med1, med2, ...]} of {status:x, data:[msg1, msg2, ...]
          utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
          // antwoord
          var payLoad = response.data;
          réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
          // het antwoord wordt teruggestuurd
          task.resolve(réponse);
        }

        // fout
        function failure(response) {
          utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
          // de status wordt geanalyseerd
          var status = response.status;
          var error;
          switch (status) {
            case 401 :
              // niet geautoriseerd
              error = 2;
              break;
            case 403:
              // verboden
              error = 3;
              break;
            case 404:
              // niet gevonden
              error = 6;
              break;
            case 0:
              // lokale fout
              error = 4;
              break;
            default:
              // iets anders
              error = 5;
          }
          // het antwoord wordt teruggestuurd
          task.resolve({err: error, messages: [response.statusText]});
        }
      }

      // --------------------- instantie van de dienst [dao]
      return {
        getData: getData
      }
}]);
  • regels 77-79: de service heeft slechts één veld: de methode [getData] waarmee informatie kan worden opgevraagd bij de webservice / JSON;
  • regel 2: er verschijnt een afhankelijkheid [$http] die we nog niet eerder zijn tegengekomen. Dit is een vooraf gedefinieerde service van Angular die de communicatie HTTP met een externe entiteit mogelijk maakt;
  • regel 6: een logboek om te zien op welk moment in de levenscyclus van de applicatie de code wordt uitgevoerd;
  • regel 10: de methode [getData] accepteert vijf parameters:
    • [serverUrl]: de hoofd-URL van de webservice (http://localhost:8080);
    • [urlAction]: de URL van de specifieke aangevraagde service (/getAllMedecins);
    • [username]: de gebruikersnaam van de gebruiker;
    • [password]: zijn wachtwoord;
    • [info]: object dat aanvullende informatie bevat wanneer de URL van de gevraagde specifieke dienst wordt opgevraagd via een POST-bewerking. In het geval van de URL (/getAllMedecins) is deze parameter niet doorgegeven. Het is dus [undefined];
  • regel 12: er wordt een asynchrone taak aangemaakt;
  • regel 14: de URL voltooit de aangevraagde dienst (http://localhost:8080/getAllMedecins);
  • regel 16: de authenticatie vindt plaats door de volgende header HTTP te verzenden:
Authorization:Basic code

waarbij [code] de Base64-code is van de tekenreeks [username:password];

Regel 16 genereert het gedeelte [Basic code] van de header HTTP;

  • regel 18: het antwoord van de webservice;
  • regel 20: de headers HTTP die standaard door Angular in een verzoek HTTP worden verzonden, worden gedefinieerd in het object [$http.defaults.headers.common]. De header [Authorization:Basic code] maakt hier geen deel van uit;
  • regel 21: deze wordt toegevoegd aan de headers HTTP die altijd moeten worden verzonden. Links van de toewijzing staat de header [Authorization] die moet worden geïnitialiseerd en rechts de waarde van de header, in dit geval de waarde die op regel 16 is gedefinieerd. Als we dus schrijven:
headers.Authorization = 'x';

zal Angular de header HTTP verzenden:

Authorization : x
  • regel 23: de methoden van de service [$http] retourneren beloften. Deze worden opgeslagen in de variabele [promise];
  • regel 27: omdat de parameter [info] hier de waarde [undefined] heeft, wordt regel 27 uitgevoerd. De URL (http://localhost:8080/getAllMedecins) wordt opgevraagd met een GET. Om niet te lang te hoeven wachten, wordt een maximale wachttijd (time-out) ingesteld voor het ontvangen van het antwoord van de server. Standaard is deze tijd één seconde;
  • regel 29: we definiëren de twee methoden die moeten worden uitgevoerd wanneer de promise wordt verkregen:
    • [success]: gedefinieerd op regel 34, is de methode die moet worden uitgevoerd wanneer de belofte wordt verkregen na een succesvolle uitvoering van de taak;
    • [failure]: gedefinieerd op regel 45, is de methode die moet worden uitgevoerd wanneer de promise wordt verkregen na het mislukken van de taak;
    • beide methoden (of beter gezegd: functies) zijn gedefinieerd binnen de functie [getData]. Dit is mogelijk in JavaScript. De variabelen die in [getData] zijn gedefinieerd, zijn bekend in de twee interne functies [success, failure];
  • regel 31: de taak die op regel 12 is aangemaakt, wordt teruggegeven. Hier moet men de aanroepende code in gedachten houden:

        promise = promise.then(function () {
          // de lijst met artsen wordt opgevraagd;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
});

In regel 3 hierboven wordt inderdaad een taak opgehaald.

  • regel 34: de functie [success] wordt later uitgevoerd, wanneer de aanroep HTTP succesvol is afgerond. Dit begrip ‘succes’ houdt verband met de eerste regel van een antwoord HTTP. Deze heeft de volgende vorm:
HTTP/1.1 code texte

De code is een tekst van drie cijfers die aangeeft of de aanroep al dan niet is geslaagd. Grofweg kan worden gesteld dat de codes 2xx en 3xx succescodes zijn, terwijl de overige codes foutcodes zijn. De tekst is een korte toelichting. Hieronder volgen twee mogelijke antwoorden, één bij succes en één bij een fout:

HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
  • regel 36: het antwoord van de server wordt op de console weergegeven. Bij de foutmelding [404 Not Found] krijg je iets als:

[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}

In dit antwoord gebruiken we alleen de velden [data], [status] en [statusText].

  • regel 38: we halen het veld [data] uit het antwoord. Dit zal een van de volgende vormen hebben:
    • {status: 0, data: [med1, med2, ...]} waarbij [medi] een object is dat een arts vertegenwoordigt (titel, voornaam, achternaam),
    • {status: n, data: [msg1, msg2, ...]} waarbij [msgi] een foutmelding is en n ongelijk is aan 0;

Image

  • regel 39: we stellen het antwoord {0,data} of {n,berichten} samen. Het eerste antwoord bevat de artsen in het veld [data]. Het tweede antwoord meldt een fout die aan de serverzijde is opgetreden. De server heeft deze fout afgehandeld, een foutcode gegenereerd in [err] en een lijst met foutmeldingen in [data]. In beide gevallen wordt een statuscode 200 teruggestuurd, wat aangeeft dat de opdracht volledig is verwerkt. Daarom worden beide gevallen in dezelfde functie verwerkt;
  • regel 41: de taak is voltooid ([task.resolve]) en er wordt een van de twee antwoorden geretourneerd:
    • {err: 0, data: [med1, med2, ...]}, waarbij [medi] een object is dat een arts vertegenwoordigt (titel, voornaam, achternaam),
    • {err: n, messages: [msg1, msg2, ...]} waarbij [msgi] een foutmelding is en n ongelijk is aan 0;

Deze code moet worden gekoppeld aan de manier waarop dit antwoord wordt opgehaald in de aanroepende code van de controller:


        // we analyseren het resultaat van de vorige aanroep
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          ...
          }

Het antwoord van [task.resolve(réponse)] is hierboven terug te vinden in de variabele [result].

  • regel 45: de functie [failure] wanneer de asynchrone taak mislukt. Er zijn twee mogelijke gevallen:
    • de server meldt deze fout door een statuscode terug te sturen die noch 2xx, noch 3xx is,
    • Angular annuleert de aanroep HTTP. Er vindt dan geen aanroep plaats. Er is een Angular-uitzondering, maar er wordt geen foutcode HTTP door de server teruggestuurd. Dit is bijvoorbeeld het geval als er een ongeldige URL wordt opgegeven die niet kan worden aangeroepen;
  • regel 46: het antwoord wordt weergegeven op de console;
  • regel 48: we herinneren ons dat het antwoord van de server de volgende vorm heeft:

{"data":"...","status":404,"config":{...},"statusText":"Not Found"}

Op regel 48 halen we het bovenstaande attribuut [status] op;

  • regels 50-70: op basis van de foutcode HTTP genereren we een nieuwe foutcode om de aard HTTP van de methode [dao.getData] te verbergen voor de aanroepende codes. We kunnen nagaan dat er in de controller die deze methode gebruikt, niets erop wijst dat er een aanroep HTTP in de methode zit;
    • regel 51: de fout [401] duidt op een mislukte authenticatie (bijvoorbeeld een onjuist wachtwoord),
    • regel 55: de fout [403] duidt op een ongeautoriseerde aanroep. De gebruiker heeft zich correct geauthenticeerd, maar beschikt niet over voldoende rechten om de URL op te vragen die hij heeft aangevraagd. Dit zal gebeuren bij de gebruiker [user / user]. Deze gebruiker bestaat wel degelijk in de database, maar heeft geen toestemming om de applicatie te gebruiken. Alleen de gebruiker [admin / admin] heeft deze toestemming;
    • regel 59: de fout [404] komt overeen met een niet-gevonden URL. De fout kan verschillende oorzaken hebben:
      • de gebruiker heeft een typefout gemaakt in de URL van de service;
      • de webservice is niet gestart;
      • de webservice heeft niet snel genoeg gereageerd (standaardtijdlimiet van één seconde);
    • regel 63: de foutcode HTTP 0 bestaat niet. Dit is het geval wanneer Angular de gevraagde aanroep HTTP niet heeft uitgevoerd omdat de door de gebruiker ingevoerde URL ongeldig is en niet kan worden aangeroepen. We zullen later nog andere gevallen tegenkomen waarin Angular de gevraagde aanroep HTTP niet uitvoert;
  • regel 72: de taak wordt succesvol afgerond (task.resolve) door een antwoord van het type {err, messages} terug te sturen, waarbij de array [messages] uitsluitend bestaat uit het bericht [response.statusText]. Als Angular de gevraagde aanroep HTTP niet heeft uitgevoerd, krijgen we een lege tekenreeks;

Nu we zowel een algemeen als een gedetailleerd overzicht van de applicatie hebben, kunnen we beginnen met het testen.

3.7.6.5. Testen van de applicatie - 1

Laten we beginnen met geldige invoer:

Image

  • bij [1] vullen we 0 in om geen wachttijd te hebben;
  • bij [2] krijgen we een foutmelding, terwijl de invoer correct is. We hebben de verschillende foutmeldingen niet weergegeven. De melding die bij [2] wordt weergegeven, is een algemene melding die hoort bij fout 0, wat overeenkomt met een Angular-uitzondering. Angular heeft een probleem ondervonden waardoor het de aanroep HTTP niet kon uitvoeren. In dergelijke gevallen moet je de logbestanden van de JavaScript-console bekijken. Er zijn twee manieren om dit te doen:
    • [F12] uitvoeren in de Chrome-browser;
    • de console van WebStorm gebruiken;

In de WebStorm-console vinden we verschillende berichten, waaronder dit:

XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Er is geen 'Access-Control-Allow-Origin'-header aanwezig voor de opgevraagde bron. De oorsprong 'http://localhost:63342' heeft daarom geen toegang.
[dao] getData[/getAllMedecins] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}
  • regel 1: Angular meldt een fout waar we later op terugkomen;
  • regel 2: het logboek van de methode [dao.getData]. Daarin staan interessante zaken:
    • [status] is gelijk aan 0, wat aangeeft dat er geen aanroep van HTTP heeft plaatsgevonden. Bijgevolg is [statusText] leeg,
    • [url] is gelijk aan [http://localhost:8080/getAllMedecins], wat correct is;
    • de header HTTP voor authenticatie [Authorization":"Basic YWRtaW46YWRtaW4=] is ook correct;

Maar waarom werkte het dan niet? De sleutelzin in de logbestanden is [No 'Access-Control-Allow-Origin' header is present]. Om dit te begrijpen, is een uitgebreide uitleg nodig. Laten we eerst even teruggaan naar de algemene architectuur van de client/server-toepassing:

Image

  • de pagina’s HTML / CSS / JS van de Angular-applicatie zijn afkomstig van de server [1];
  • in [2] doet de service [dao] een verzoek aan een andere server, namelijk de server [2]. Welnu, dat wordt verboden door de browser die de Angular-applicatie uitvoert, omdat het een beveiligingslek is. De applicatie mag alleen de server opvragen waarvan ze afkomstig is, d.w.z. de server [1];

Eigenlijk is het onjuist om te zeggen dat de browser de Angular-applicatie verbiedt om de server [2] te benaderen. Ze benadert deze server juist om te vragen of hij een client toestaat die niet van zijn eigen domein afkomstig is om hem te benaderen. Deze techniek voor het delen van bronnen wordt CORS (Cross-Origin Resource Sharing) genoemd. De server [2] geeft toestemming door specifieke HTTP-headers te verzenden. Omdat onze server [2] deze headers hier niet heeft verzonden, weigerde de browser de door de applicatie gevraagde aanroep HTTP uit te voeren.

Laten we nu eens in detail kijken. Laten we eens kijken naar de netwerkverkeer dat plaatsvond tijdens de aanroep HTTP. Hiervoor drukken we in de Chrome-browser op [F12] om de ontwikkelaarstools te openen en selecteren we het tabblad [Network] om het netwerkverkeer te bekijken:

  • in [1] selecteren we het tabblad [network];
  • in [2] vragen we de lijst met artsen op;

We krijgen de volgende informatie te zien in het tabblad [network]:

  • in [1], de informatie die naar de server is verzonden;
  • in [2], het antwoord van de server;

In [1] is te zien dat de browser een verzoek HTTP [OPTIONS] heeft verzonden voor de gevraagde URL. [OPTIONS] is een van de mogelijke HTTP-commando's, naast de bekendere [GET] en [POST]. Hiermee kan informatie worden opgevraagd bij een server, met name over de HTTP-opties die deze ondersteunt, vandaar de naam van het commando. De server geeft zijn antwoord in [2]. Om aan te geven dat hij verzoeken van clients accepteert die niet tot zijn domein behoren, moet hij een specifieke header terugsturen, genaamd [Access-Control-Allow-Origin]. En juist omdat hij deze niet heeft teruggestuurd, heeft Angular de gevraagde aanroep HTTP niet uitgevoerd en de volgende foutmelding teruggestuurd:

XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Er is geen 'Access-Control-Allow-Origin'-header aanwezig bij de opgevraagde bron. De oorsprong 'http://localhost:63342' krijgt daarom geen toegang.

We moeten onze server dus aanpassen zodat deze de verwachte header HTTP verstuurt.

3.7.6.6. Aanpassing van de webserver / JSON

We gaan terug naar Eclipse. Om het bereikte resultaat te behouden, dupliceren we de huidige versie van de webserver / JSON [rdvmedecins-webapi-v2] naar [rdvmedecins-webapi-v3] [1]:

We brengen een eerste wijziging aan in [ApplicationModel], een van de configuratie-elementen van de webservice:


package rdvmedecins.web.models;

...

@Component
public class ApplicationModel implements IMetier {

    // de laag [métier]
    @Autowired
    private IMetier métier;

    // gegevens afkomstig van de laag [métier]
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // configuratiegegevens
    private boolean CORSneeded = true;

...

    public boolean isCORSneeded() {
        return CORSneeded;
    }

}
  • regel 17: we maken een booleaanse variabele aan die aangeeft of klanten van buiten het domein van de server worden geaccepteerd of niet;
  • regels 21-23: de methode om toegang te krijgen tot deze informatie;

Vervolgens maken we een nieuwe Spring-controller aan: MVC [3]:

De klasse [RdvMedecinsCorsController] ziet er als volgt uit:


package rdvmedecins.web.controllers;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import rdvmedecins.web.models.ApplicationModel;

@Controller
public class RdvMedecinsCorsController {

    @Autowired
    private ApplicationModel application;

    // verzending van opties naar de client
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // de header wordt ingesteld CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
        }

    }

    // lijst met artsen
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • regels 28-31: definiëren een controller voor de URL [/getAllMedecins] wanneer deze wordt aangeroepen met de opdracht HTTP [OPTIONS];
  • regel 29: de methode [getAllMedecins] accepteert als parameter het object [HttpServletResponse], dat naar de klant wordt verzonden die het verzoek heeft gedaan. Dit object wordt door Spring geïnjecteerd;
  • regel 30: de verwerking van het verzoek wordt gedelegeerd aan de privémethode in de regels 19-25;
  • regels 15-16: het object [ApplicationModel] wordt geïnjecteerd;
  • regels 20-23: als de server is geconfigureerd om clients van buiten zijn domein te accepteren, dan wordt de header HTTP verzonden:

Access-Control-Allow-Origin: *

wat betekent dat de server clients van elk domein (*) accepteert.

We zijn nu klaar voor nieuwe tests. We lanceren de nieuwe versie van de webservice en ontdekken dat het probleem nog steeds bestaat. Er is niets veranderd. Als we in regel 30 hierboven een console-uitvoer plaatsen, wordt deze nooit weergegeven, wat aantoont dat de methode [getAllMedecins] uit regel 29 nooit wordt aangeroepen.

Na wat onderzoek ontdekken we dat Spring MVC de opdrachten HTTP en [OPTIONS] zelf verwerkt met een standaardverwerking. Het is dus altijd Spring die reageert en nooit de methode [getAllMedecins] op regel 29. Dit standaardgedrag van Spring MVC kan worden gewijzigd. We introduceren een nieuwe configuratieklasse om het nieuwe gedrag in te stellen:

  

De nieuwe configuratieklasse [WebConfig] ziet er als volgt uit:


package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    // configuratie van dispatcherservlet voor de headers CORS
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
}
  • regel 8: de klasse is een Spring-configuratieklasse. Hierin worden beans gedeclareerd die in de Spring-context worden geplaatst;
  • regel 12: de bean [dispatcherServlet] dient om de servlet te definiëren die de verzoeken van de clients afhandelt. Deze is van het type [DispatcherServlet]. Deze servlet wordt normaal gesproken standaard aangemaakt. Als we deze zelf aanmaken, kunnen we deze vervolgens configureren;
  • regel 14: we maken een instantie van het type [DispatcherServlet] aan;
  • regel 15: we geven aan dat de servlet de commando’s HTTP en [OPTIONS] naar de applicatie moet doorsturen;
  • regel 16: we maken de servlet op deze manier geconfigureerd;

Nu moeten we nog de klasse [AppConfig] aanpassen:


package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {

}
  • regel 11: de nieuwe configuratieklasse [WebConfig] wordt geïmporteerd;

3.7.6.7. Testen van de applicatie - 2

We starten de nieuwe versie van de webservice / JSON en proberen de lijst met artsen op te halen met onze Angular-client. We bekijken de netwerkverkeer in het tabblad [Network]:

  • in [1] is te zien dat de header HTTP [Access-Control-Allow-Origin: *] nu in het antwoord van de server aanwezig is. En toch werkt het nog steeds niet. We bekijken in [2] de logbestanden van de console. Daar vinden we het volgende logbericht:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Het verzoekheaderveld Authorization is niet toegestaan door Access-Control-Allow-Headers

We zien dat de browser wacht op een nieuwe header HTTP [Access-Control-Allow-Headers], die hem zou laten weten dat we toestemming hebben om de authenticatieheader naar hem te sturen:

Authorization:Basic code

Dit kan een goed teken zijn. Angular wilde misschien het commando HTTP GET verzenden. Maar aangezien dit vergezeld gaat van een authenticatieheader, vraagt de browser of de server deze accepteert.

We passen onze webserver / JSON aan om deze header te verzenden. De klasse [RdvMedecinsCorsController] wordt als volgt aangepast:


    // opties naar de client verzenden
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // de header CORS wordt ingesteld
            response.addHeader("Access-Control-Allow-Origin", "*");
            // de header wordt toegestaan [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");            
}
  • in de regels 6-7 wordt de ontbrekende header toegevoegd.

We starten de server opnieuw op en vragen de lijst met artsen opnieuw op met de Angular-client:

 

Deze keer lukt het wel. De console-logs tonen het antwoord dat is ontvangen door de methode [dao.getData]:


[dao] getData[/getAllMedecins] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}

We zien dat:

  • de server een foutcode [status=200] heeft teruggestuurd met het bericht [statusText=OK]. Daarom bevinden we ons in de functie [success];
  • de server heeft een object [data] teruggestuurd met twee velden:
    • [status]: (niet te verwarren met de foutcode HTTP [status]). Hier geeft [status=0] aan dat URL en [/getAllMedecins] foutloos zijn verwerkt;
    • [data]: deze bevat de lijst JSON met artsen;

Laten we nu enkele andere interessante gevallen bekijken:

Er zit een fout in de identificatiecodes [login, password]:

We loggen in met de identiteit [user / user], die geen toegang heeft tot de applicatie (alleen [admin] heeft toegang):

Deze keer is de fout niet langer [Erreur d'authentification], maar [Accès refusé].

3.7.7. Voorbeeld 7: klantenlijst

We gebruiken de vorige toepassing opnieuw om deze keer de klantenlijst weer te geven in een vervolgkeuzelijst van het type [Bootstrap select] (zie paragraaf 3.6.6).

3.7.7.1. Weergave V

De beginweergave ziet er als volgt uit:

 

Om de weergave V te verkrijgen, dupliceren we de code [app-16.html] in [app-17.html] en passen we deze als volgt aan:


<div class="container" >
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible" >
...
  </div>

  <!-- de aanvraag -->
  <div class="alert alert-info" ng-hide="waiting.visible" >
...
    <button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
  </div>

  <!-- de klantenlijst -->
  <div class="row" style="margin-top: 20px" ng-show="clients.show">
    <div class="col-md-3">
      <h2 translate="{{clients.title}}"></h2>
      <select data-style="btn-primary" class="selectpicker">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
      </select>
    </div>
  </div>

  <!-- de foutenlijst -->
  <div class="alert alert-danger"  ng-show="errors.show">
   ...
  </div>

</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
  • regels 5-7: de wachtbanner blijft ongewijzigd;
  • regels 10-13: het formulier verandert niet, behalve de tekst op de knop (regel 12);
  • regels 28-30: de foutmelding verandert niet;
  • regels 16-25: de klanten worden weergegeven in een vervolgkeuzelijst die wordt opgemaakt door de component [Bootstrap-selectpicker] (attributen data-style, class, regel 19);
  • regel 20: de richtlijn [ng-repeat] wordt gebruikt om de verschillende opties van de vervolgkeuzelijst te genereren. Merk op dat de tekst van een optie van het type [Mme Julienne Tatou] is en dat de waarde van de optie van het type [100] is, waarbij 100 de id van de weergegeven klant is;
  • regel 34: de JavaScript-code wordt verplaatst naar een nieuw bestand [rdvmedecins-05];

3.7.7.2. De C-controller en het M-model

De JavaScript-code van het bestand [rdvmedecins-05] wordt verkregen door het bestand [rdvmedecins-04] te kopiëren:

Image

Er verandert vrijwel niets, behalve in de controller, die nu is aangepast om de lijst met klanten weer te geven:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- initialisatie van het sjabloon
      // sjabloon
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      $scope.server = {url: undefined, login: undefined, password: undefined};
      $scope.clients = {title: config.listClients, show: false, model: {}};
      $scope.errors = {show: false, model: {}};
      $scope.urlServerLabel = config.urlServerLabel;
      $scope.loginLabel = config.loginLabel;
      $scope.passwordLabel = config.passwordLabel;

      // asynchrone taak
      var task;

      // actie uitvoeren
      $scope.execute = function () {
        // UI wordt bijgewerkt
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // gesimuleerde wachttijd
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // wachtrij
        promise = promise.then(function () {
          // de klantenlijst wordt opgevraagd;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
          return task.promise;
        });
        // het resultaat van de vorige aanroep wordt geanalyseerd
        promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // de verzamelde gegevens worden in het model geplaatst
            $scope.clients.data = result.data;
            // de UI wordt bijgewerkt
            $scope.clients.show = true;
            $scope.waiting.visible = false;
            // de vervolgkeuzelijst wordt opgemaakt
            $('.selectpicker').selectpicker();
          } else {
            // er zijn fouten opgetreden bij het ophalen van de klantenlijst
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // het bestand UI wordt bijgewerkt
            $scope.waiting.visible = false;
          }
        });
      };

      // afwachting annulering
      function cancel() {
        // de taak wordt voltooid
        task.reject();
        // het bestand UI wordt bijgewerkt
        $scope.waiting.visible = false;
        $scope.clients.show = false;
        $scope.errors.show = false;
      }
    }
  ])
;
  • Er verandert heel weinig in de controller. Deze leverde voorheen een lijst met artsen. Nu levert deze een lijst met klanten;
  • regel 9: [$scope.clients] wordt het sjabloon voor de klantenbalk in de V-weergave;
  • regel 30: het is nu URL [/getAllClients] dat wordt gebruikt;
  • regels 35-36: de twee vormen van het antwoord dat door de methode [dao.getData] wordt geretourneerd. We hebben nu klanten in plaats van artsen;
  • regel 44: een vrij zeldzame instructie in Angular-code. We manipuleren het DOM (Document Object Model) rechtstreeks. Hier willen we de methode [selectpicker] (onderdeel van [bootstrap-select.min.js]) toe te passen op de elementen van DOM die de klasse [selectpicker] [$('.selectpicker')] hebben. Er is er maar één, de vervolgkeuzelijst:

      <select data-style="btn-primary" class="selectpicker" select-enable="">
....
      </select>

In paragraaf 3.6.6 is aangetoond dat dit de vervolgkeuzelijst als volgt opmaakte:

Net zoals bij de artsen moeten we ook de webservice aanpassen.

3.7.7.3. Aanpassing van de webservice - 1

  

De klasse [RdvMedecinsController] wordt uitgebreid met een nieuwe methode:


package rdvmedecins.web.controllers;

...

@Controller
public class RdvMedecinsCorsController {

    @Autowired
    private ApplicationModel application;

    // opties naar de klant verzenden
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // de header wordt ingesteld CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // de header wordt geautoriseerd [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");
        }

    }

    // lijst met artsen
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }

    // lijst met klanten
    @RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
    public void getAllClients(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • regels 29-32: de methode [getAllClients] verwerkt de aanvraag HTTP [OPTIONS] die de browser naar deze methode stuurt;

3.7.7.4. Testen van de applicatie – 1

We zijn nu klaar om te testen. We starten de webserver en voeren vervolgens geldige waarden in het Angular-formulier in. We krijgen het volgende antwoord:

Image

Deze foutmelding wordt weergegeven wanneer Angular de gevraagde verzoek HTTP niet heeft kunnen uitvoeren. De oorzaken hiervan moeten dan worden gezocht in de logbestanden van de console. Daarin staat het volgende bericht:

XMLHttpRequest cannot load http://localhost:8080/getAllClients. Er is geen 'Access-Control-Allow-Origin'-header aanwezig op de opgevraagde bron. De oorsprong 'http://localhost:63342' krijgt daarom geen toegang.

Een probleem waarvan we dachten dat het was opgelost. Laten we daarom eens kijken naar de netwerkverkeer dat heeft plaatsgevonden:

Image

We zien dat de bewerking [getAllClients] met de methode HTTP [OPTIONS]goed is verlopen, maar dat de bewerking [getAllClients] met de methode HTTP [GET] is geannuleerd. Het antwoord op het verzoek [OPTIONS] was als volgt:

Image

De headers HTTP van CORS zijn aanwezig. Laten we nu de uitwisselingen HTTP tijdens GET bekijken:

Image

Het verzoek HTTP lijkt correct. We zien met name de authenticatieheader.

Naast de eerdere foutmelding staat in de consolelogbestanden de volgende melding:


[dao] getData[/getAllClients] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}

Dit is het logboek dat de methode [dao.getData] systematisch aanmaakt bij ontvangst van het antwoord op haar verzoek HTTP. Er vallen twee dingen op:

  • [status=0]: dit betekent dat Angular het verzoek HTTP heeft geannuleerd;
  • [method=GET]: en het is het verzoek GET dat is geannuleerd;

In combinatie met het eerste bericht betekent dit dat Angular ook voor het verzoek GET hier de headers CORS verwacht. Op dit moment verstuurt onze webservice deze echter alleen voor de verzoeken HTTP en [OPTIONS]. Het is heel vreemd dat deze fout nu optreedt en niet bij de lijst met artsen. Ik heb hier geen verklaring voor.

De webservice moet dus opnieuw worden aangepast.

3.7.7.5. Aanpassing van de webservice – 2

  

De methoden [GET] en [POST] worden verwerkt in de klasse [RdvMedecinsController]. We moeten deze aanpassen zodat deze methoden de headers CORS verzenden. Dit doen we als volgt:


@RestController
public class RdvMedecinsController {

    @Autowired
    private ApplicationModel application;

    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;

...

    // lijst met klanten
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients(HttpServletResponse response) {
        // headers CORS
        rdvMedecinsCorsController.getAllClients(response);
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // klantenlijst
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
    }
...
  • regel 8: we willen de code hergebruiken die we in de controller [RdvMedecinsCorsController] hebben geplaatst. Daarom voegen we deze hier in;
  • regel 14: de methode die het verzoek verwerkt: [GET /getAllClients]. We brengen twee wijzigingen aan:
    • regel 14: we injecteren het object [HttpServletResponse] in de parameters van de methode,
    • regel 16: we gebruiken de methoden van de klasse [RdvMedecinsCorsController] om de headers CORS in dit object op te nemen;

3.7.7.6. Testen van de applicatie – 2

We starten de nieuwe versie van de webservice en vragen opnieuw de lijst met klanten op. We krijgen het volgende antwoord:

  • in [1] hebben we wel een antwoord, maar het is leeg [2];
  • bij [3]: de netwerkcommunicatie is goed verlopen;

In de consolelogboeken heeft de methode [dao.getData] het ontvangen antwoord weergegeven:


[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"} 

De methode heeft de lijst met klanten dus wel degelijk ontvangen. Nadat de code is gecontroleerd, richten we onze aandacht op de volgende instructie, die we niet zo goed beheersen:


// de vervolgkeuzelijst opmaken
$('.selectpicker').selectpicker();

We zetten regel 2 tussen haakjes en proberen het opnieuw. We krijgen dan het volgende antwoord:

We hebben het probleem dus gelokaliseerd. Het is de toepassing van de methode [selectpicker] op de vervolgkeuzelijst die voor problemen zorgt. Als we de broncode van de foutieve pagina bekijken, zien we het volgende:

  • zien we dat in [1] de keuzelijst wel degelijk aanwezig is met zijn elementen, maar dat deze niet wordt weergegeven [style='display:none'];
  • in [2] zien we dat de knop [bootstrap select] wel wordt weergegeven. De items van de vervolgkeuzelijst zouden in de lijst <ul role='menu'> moeten verschijnen. Ze staan er niet in en daarom is de lijst leeg. Het lijkt erop dat toen de methode [selectpicker] op de vervolgkeuzelijst werd toegepast, de inhoud ervan op dat moment leeg was;

Bij het zoeken naar een oplossing op internet vinden we deze. We vervangen de code:


// de vervolgkeuzelijst opmaken
$('.selectpicker').selectpicker();

door de volgende:


            // de vervolgkeuzelijst opmaken
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

De stijl [bootstrap-select] wordt toegepast via een functie [$timeout]. We zijn deze functie al eerder tegengekomen; hiermee kan een functie na een bepaalde vertraging worden uitgevoerd. In dit geval staat het ontbreken van een vertraging gelijk aan een vertraging van nul. De voorgaande regels plaatsen een gebeurtenis in de wachtrij van de browser. Zodra de verwerking van de huidige gebeurtenis (klik op de knop [Liste des clients]) is voltooid, wordt weergave V getoond. Vervolgens controleert de browser onmiddellijk daarna zijn lijst met gebeurtenissen. Vanwege de nulvertraging staat de gebeurtenis [$timeout] bovenaan de lijst en wordt deze verwerkt. De stijl [bootstrap-select] wordt dan toegepast op een ingevulde vervolgkeuzelijst. Laten we het resultaat eens bekijken:

Als we nogmaals naar de broncode van de weergegeven pagina kijken, zien we het volgende:

De knop [bootstrap-select], die eerder leeg was, bevat nu de lijst met klanten.

3.7.7.7. Gebruik van een richtlijn

In de C-controller van de V-weergave zijn we de volgende code tegengekomen:


            // de vervolgkeuzelijst opmaken
            $('.selectpicker').selectpicker();

Er wordt gewerkt met een object van het type DOM. Veel Angular-ontwikkelaars hebben een hekel aan het werken met DOM in de code van een controller. Voor hen moet dit in een richtlijn gebeuren. Een Angular-richtlijn kan worden gezien als een uitbreiding van de taal HTML. Zo is het mogelijk om nieuwe HTML-elementen of -attributen te maken. Laten we een eerste voorbeeld bekijken:

We maken het volgende JS [selectEnable]-bestand aan:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
  return {
    link: function (scope, element, attrs) {
      $timeout(function () {
        var selectpicker = $('.selectpicker');
        selectpicker.selectpicker();
      });
    }
  };
}]);
  • De richtlijn volgt de syntaxis van de controller waaraan we inmiddels gewend zijn:

angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)

De richtlijn behoort tot de module [rvmedecins]. Het is een functie die twee parameters accepteert:

  • (vervolg)
    • de eerste is de naam van de richtlijn [selectEnable];
    • de tweede is een array ['obj1','obj2',..., function(obj1, obj2,...)], waarbij de [obj] de objecten zijn die in de functie moeten worden ingebracht. Hier is het enige ingebrachte object het vooraf gedefinieerde object [$timeout];
  • de functie [directive] retourneert een object dat verschillende attributen kan hebben. Hier is het enige attribuut het attribuut [link] (regel 3). De waarde ervan is hier een functie die drie parameters accepteert:
    • scope: het sjabloon van de weergave waarin de richtlijn wordt gebruikt;
    • element: het element in de weergave, het object van de richtlijn;
    • attrs: de attributen van dit element;

Laten we een voorbeeld nemen. De richtlijn [selectEnable] zou in de volgende context kunnen worden gebruikt:

<div select-enable="data"></div>

Hierboven past het attribuut [select-enable] de richtlijn [selectEnable] toe op het element HTML <div>. Een richtlijn [doSomething] kan op elk element HTML worden toegepast door het attribuut [do-something] toe te voegen. Let op het verschil in schrijfwijze tussen de naam van de richtlijn en het bijbehorende attribuut. De schrijfwijze verandert van [camelCase] naar [camel-case].

De richtlijn [selectEnable] zou ook op de volgende manier kunnen worden gebruikt:

<select-enable attr1='val1' attr2='val2' ...>...</select-enable>

Hier wordt de richtlijn [doSomething] toegepast in de vorm van een tag HTML <do-something>.

Laten we teruggaan naar de code

<div select-enable="data"></div>

en de drie parameters van de functie [link] van de richtlijn, [scope, element, attrs]:

  • scope: is het sjabloon van de weergave waarin de <div> zich bevindt;
  • element: is de <div> zelf;
  • attrs: is de array met attributen van de <div>. Deze kunnen worden gebruikt om informatie door te geven aan de richtlijn. Hierboven schrijven we attrs['selectEnable'] om de informatie [data] te verkrijgen. Let goed op de gewijzigde schrijfwijze [selectEnable] om het attribuut [select-enable] aan te duiden;

Laten we teruggaan naar de code van de richtlijn:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
  return {
    link: function (scope, element, attrs) {
      $timeout(function () {
        $('.selectpicker').selectpicker();
      });
    }
  };
}]);
  • regels 14-16: hier vinden we de code terug die we eerder in de controller hadden geplaatst. Deze wordt uitgevoerd wanneer de richtlijn [select-enable] (in de vorm van een element of attribuut) wordt aangetroffen bij het weergeven van de weergave V.

Om deze richtlijn te implementeren, kopiëren we het bestand [app-17.html] naar [app-17B.html] en passen we het als volgt aan:


      <select data-style="btn-primary" class="selectpicker" select-enable="">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
</select>
  • regel 1: we passen de richtlijn [selectEnable] toe op het element HTML [select]. Aangezien er geen informatie aan de richtlijn hoeft te worden doorgegeven, schrijven we simpelweg [select-enable=""];

We passen ook de controller aan door het bestand JS [rdvmedecins-05.js] te dupliceren naar [rdvmedecins-05B.js] en we verwijzen naar het nieuwe bestand JS in het richtlijndossier [app-17B.html] en het richtlijndossier [selectEnable.js]. Dit laatste punt mag niet worden vergeten. Als het richtlijndossier ontbreekt, wordt het attribuut [select-enable=""] niet verwerkt, maar Angular zal geen fout melden.


<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>

In het bestand JS [rdvmedecins-05B.js] verwijderen we de volgende regels uit de controller:


            // de vervolgkeuzelijst opmaken
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

aangezien deze bewerking nu door de richtlijn wordt uitgevoerd.

3.7.7.8. Testen van de applicatie – 3

Bij het testen van de nieuwe applicatie [app-17B.html] krijgen we het volgende resultaat:

  • in [1] krijgt men een lege lijst.

De consolelogboeken geven het volgende weer:

1
2
3
[dao] init
directive selectEnable
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
  • regel 1: initialisatie van de service [dao];
  • regel 2: bij de eerste weergave van de V-weergave wordt de richtlijn [selectEnable] uitgevoerd;
  • regel 3: deze regel verschijnt wanneer de gebruiker op de knop [Liste des clients] klikt. We zien dan dat de instructie [selectEnable] niet nogmaals wordt uitgevoerd. Uiteindelijk is deze uitgevoerd toen de klantenlijst leeg was en hebben we dus een lege vervolgkeuzelijst;

Met andere woorden ert de bewerking:


$('.selectpicker').selectpicker();

niet op het juiste moment heeft plaatsgevonden. We kunnen op verschillende manieren proberen het probleem op te lossen. Na talrijke mislukte tests komen we tot de conclusie dat de bovenstaande bewerking slechts één keer mag plaatsvinden, en uitsluitend wanneer de vervolgkeuzelijst is gevuld. Om dit resultaat te bereiken, herschrijven we de tag <select> als volgt:


      <select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
</select>

Regel 1: de tag <select> wordt alleen gegenereerd als [clients.data] bestaat. Dit is niet het geval bij de eerste weergave van de V-weergave. De tag <select> wordt dus niet gegenereerd en de instructie [selectEnable] wordt niet uitgevoerd. Wanneer de gebruiker op de knop [Liste des clients] klikt, krijgt [clients.data] een nieuwe waarde in het model M. Omdat het model M is gewijzigd, wordt de tag <select> opnieuw geëvalueerd en in dit geval gegenereerd. De richtlijn [selectEnable] wordt dus ook geëvalueerd. Op het moment dat deze wordt geëvalueerd, zijn de regels 2-4 van de tag <select> nog niet geëvalueerd. We hebben dus een lege klantenlijst. Als we de richtlijn [selectEnable] als volgt schrijven:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable");
      $('.selectpicker').selectpicker();
    }
  }
}]);

wordt regel 5 uitgevoerd met een lege lijst en krijgen we een lege keuzelijst te zien. We moeten dan het volgende schrijven:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable");
      $timeout(function () {
        $('.selectpicker').selectpicker();
      })
    }
  }
}]);

om het verwachte resultaat te krijgen. Vanwege de [$timeout] in regel 5 wordt regel 6 pas uitgevoerd nadat de weergave V volledig is geëvalueerd, dus op een moment waarop de tag <select> alle elementen bevat.

3.7.8. Voorbeeld 8: de agenda van een arts

We presenteren nu een applicatie die de agenda van een arts weergeeft.

3.7.8.1. De weergave V van de applicatie

We zullen het volgende formulier presenteren:

  • in [1] wordt de agenda opgevraagd van mevrouw PELISSIER [2], op 25 juni 2014 [3];

Het volgende resultaat wordt verkregen: [4]:

We zullen de twee weergaven afzonderlijk bekijken.

3.7.8.2. Het formulier

We dupliceren het bestand [app-17.html] naar [app-18.html] en passen vervolgens de code als volgt aan:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- de aanvraag -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="row" style="margin-bottom: 20px">
      <div class="col-md-3">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" class="selectpicker">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
      <div class="col-md-3">
        <h2 translate="{{calendar.title}}"></h2>
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
                      class="well well-sm"></datepicker>
        </div>
      </div>
    </div>
    <button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
  </div>

  <!-- de foutenlijst -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>

  <!-- de agenda -->
  <div id="agenda" ng-show="agenda.show">
...
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
  • regels 5-7: het wachtbericht verandert niet;
  • regels 12-19: de lijst met artsen van het type [bootstrap select];
  • regels 20-26: de agenda van [ui-bootstrap] die we al hebben laten zien. Merk op dat de geselecteerde dag in het model [calendar.jour] wordt geplaatst (attribuut ng-model);
  • regel 28: de knop waarmee de agenda wordt opgevraagd;
  • regels 32-34: de lijst met fouten blijft ongewijzigd;
  • regels 37-39: de agenda die we later zullen bespreken;
  • regel 42: de code JS wordt overgebracht naar het bestand [rdvmedecins-06.js] door het bestand [rdvmedecins-05.js] te kopiëren;

3.7.8.3. De controller C

De code JS van de applicatie wordt als volgt:

Image

Alleen de service [utils] en de controller [rdvMedecinsCtrl] worden door de wijzigingen beïnvloed.

De controller [rdvMedecinsCtrl] wordt als volgt:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- initialisatie van het model
      // sjabloon
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      $scope.errors = {show: false, model: {}};
      $scope.medecins = {
        data: [
          {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
          {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
          {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
          {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
        ],
        title: config.listMedecins};
      $scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
      $scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
      // de vervolgkeuzelijst opmaken
      $timeout(function () {
        $('.selectpicker').selectpicker();
      });
      // Franse taalinstelling voor de kalender
      angular.copy(config.locales['fr'], $locale);
 ...
    }
  ])
;
  • regel 7: er wordt een wachttijd van 3 seconden ingesteld voordat de aanroep HTTP wordt uitgevoerd;
  • regel 8: de benodigde elementen voor de verbinding met HTTP worden hard gecodeerd;
  • regels 10-17: de lijst met artsen wordt hard gecodeerd;
  • regel 18: het sjabloon [agenda] configureert de weergave van de agenda in de weergave;
  • regel 19: het sjabloon [calendar] configureert de weergave van de kalender in de weergave. We stellen een minimumdatum [minDate] in op vandaag en de huidige datum eveneens op vandaag;
  • regels 21-23: de vervolgkeuzelijst wordt opgemaakt volgens de eerder beschreven methode;
  • regel 25: de taalinstelling van de applicatie wordt ingesteld op 'fr'. Standaard is deze ingesteld op 'en';

De methode die wordt uitgevoerd bij het opvragen van de agenda is als volgt:


// actie uitvoeren
      $scope.execute = function () {
        // de gegevens van het formulier
        var idMedecin = $('.selectpicker').selectpicker('val');

        // controle
        utils.debug("[homeCtrl] idMedecin", idMedecin);
        utils.debug("[homeCtrl] jour", $scope.calendar.jour);

        // de datum wordt weergegeven in de notatie jjjj-MM-dd
        var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
        // de weergave wordt bijgewerkt
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        $scope.agenda.show = false;
...
      };
  • regel 4: we halen het attribuut [value] op van de geselecteerde arts. Hier gebruiken we opnieuw de methode [selectpicker], die afkomstig is uit het bestand [bootstrap-select.min.js]. Houd rekening met de vorm van de opties in de vervolgkeuzelijst:

          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}

De waarde (attribuut value) van de optie is dus de identificatiecode [id] van de arts.

  • regel 11: de door de gebruiker gekozen dag wordt omgezet naar het formaat [aaaa-mm-jj], het datumformaat dat de webserver verwacht;
  • regels 13-15: zodra de methode [execute] is voltooid, wordt de laadbalk weergegeven en wordt al het overige verborgen;

De code gaat als volgt verder:


// gesimuleerde wachttijd
        var task = utils.waitForSomeTime($scope.waiting.time);
        // de agenda van de arts opvragen
        var promise = task.promise.then(function () {
          // het pad naar de dienst-URL
          var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
          // de agenda wordt opgevraagd
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
          // de voltooiingsbelofte van de taak wordt teruggestuurd
          return task.promise;
        });
        // het resultaat van de aanroep van de service [dao] wordt geanalyseerd
        promise.then(function (result) {
          // einde van het wachten
          $scope.waiting.visible = false;
          // fout?
          if (result.err == 0) {
            // de agenda-sjabloon wordt voorbereid
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            // opmaak van de weergave van de tijden
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
            // er wordt een gebeurtenis aangemaakt om de tabel op te maken na het weergeven van de weergave
            $timeout(function () {
              $("#creneaux").footable();
            });
          } else {
            // er zijn fouten opgetreden bij het ophalen van de agenda
            $scope.errors = {
              title: config.getAgendaErrors,
              messages: utils.getErrors(result),
              show: true
            };
}
  • regel 2: de asynchrone wachttijd van 3 seconden;
  • regels 5-10: de code die wordt uitgevoerd zodra deze wachttijd is verstreken;
  • regel 6: we construeren de opgevraagde URL [/getAgendaMedecinJour/1/2014-06-25];
  • regel 8: de URL wordt opgevraagd. Er wordt een asynchrone taak gestart;
  • regel 10: de belofte van deze asynchrone taak wordt teruggegeven;
  • regels 14-38: de code die wordt uitgevoerd zodra de aanroep HTTP zijn antwoord heeft teruggestuurd;
  • regel 13: [result] is het antwoord dat door de methode [dao.getData] is verzonden. Hier moet je rekening houden met de vorm van het antwoord van de webserver:

De parameter [result.data] op regel 19 is het hierboven genoemde attribuut [data] [1]. Dit attribuut bevat op zijn beurt het hierboven genoemde attribuut [creneauxMedecin] [2]. Dit is een tabel met tijdvakken, waarbij voor elk tijdvak de volgende twee gegevens zijn opgenomen:

  • [rv]: de vorm JSON van een afspraak of [null] als er geen afspraak is ingepland voor dit tijdslot;
  • [hDeb, mDeb, hFin, mFin]: de tijdsgegevens van het tijdvak;

Laten we teruggaan naar de code van de controller:

  • regel 15: het wachten is voltooid;
  • regel 19: het model [$scope.agenda] wordt ingevuld, dat de weergave van de agenda regelt;
  • regel 20: de agenda wordt zichtbaar gemaakt;
  • regels 22-24: we doorlopen elk van de C-elementen van de tabel [creneauxMedecin] waar we het zojuist over hadden;
  • regel 23: elk C-element heeft een attribuut [creneau] dat het tijdvak aangeeft. Dit wordt aangevuld met een attribuut [text] dat de tekstuele weergave van het tijdvak zal zijn in de vorm [10h20:10h40];
  • regels 26-28: we maken de tabel HTML, die wordt gebruikt om de tijdvakken van de agenda weer te geven, ‘responsive’. We hebben dit begrip in paragraaf 3.6.7 besproken;
 
  • regel 27: om de tabel 'responsive' te maken, moet de methode [footable] erop worden toegepast. Hier stuit men op dezelfde moeilijkheid als bij de component [bootstrap-select]. Als men simpelweg regel 17 schrijft, merkt men dat de tabel niet 'responsive' is. Dit probleem los je op dezelfde manier op met de functie [$timeout] (regel 26);
  • regels 31-34: het geval waarin de aanroep van HTTP is mislukt. In dat geval worden de foutmeldingen weergegeven;

3.7.8.4. Weergave van de agenda

We keren nu terug naar de code van de agenda in het bestand [app-18.html]. Deze luidt als volgt:


<!-- de agenda -->
  <div id="agenda" ng-show="agenda.show">
    <!-- geval van een arts zonder consulttijdvakken -->
    <h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
        translate="agenda_medecinsanscreneaux"></h4>
    <!-- agenda van de arts -->
    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table creneaux-table id="creneaux" class="table">
          <thead>
          <tr>
            <th data-toggle="true">
              <span translate="agenda_creneauhoraire"></span>
            </th>
            <th>
              <span translate="agenda_client">Client</span>
            </th>
            <th data-hide="phone">
              <span translate="agenda_action">Action</span>
            </th>
          </tr>
          </thead>
          <tbody>
          <tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
            <td>
            <span
              ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro status-suspended'">
              {{creneauMedecin.creneau.text}}
            </span>
            </td>
            <td>
              <span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}} {{creneauMedecin.rv.client.nom}}</span>
            </td>
            <td>
              <a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active">
              </a>
              <a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended">
              </a>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
</div>
  • regels 4-5: we herinneren ons dat [agenda.data] de agenda is, en dat [agenda.data.creneauxMedecin] een array is van objecten van het type [creneauMedecin]. Elk element van dit laatste type heeft een attribuut [creneauMedecin.creneau] dat een tijdslot is. Elk tijdslot bevat twee elementen die voor ons van belang zijn:
    • [creneauMedecin.creneau.rv], dat de eventuele RV (rv!=null) is die bij het tijdslot hoort;
    • [creneauMedecin.creneau.text], dat de tekst [début:fin] van het tijdslot is;
  • regel 4: geeft een speciaal bericht weer als de arts geen tijdvakken heeft. Dit is onwaarschijnlijk, maar onze database is onvolledig en dit geval komt voor. Of het bericht HTML al dan niet wordt gegenereerd, wordt bepaald door de richtlijn [ng-if];

Image

De richtlijn [ng-if] verschilt van de richtlijnen [ng-show, ng-hide]. Deze laatste verbergen slechts een veld dat in het document aanwezig is. Als [ng-if='false'] geldt, wordt het veld uit het document verwijderd. We hebben deze hier ter illustratie gebruikt;

  • regel 9: het attribuut [id='creneaux'] is belangrijk. Dit wordt gebruikt in de instructie:

$("#creneaux").footable();
  • regels 10-22: geven de kopteksten van de tabel [1] weer;
  • regels 23-45: geven de inhoud van de tabel [2] weer;
  • regel 24: doorloop de tabel [agenda.data.creneauxMedecin];
  • regels 26-29: de tekst [3] wordt geschreven. Er wordt gebruikgemaakt van de richtlijn [ng-class], die het attribuut [class] van het element genereert. Als hier [creneauMedecin.rv==null] staat, betekent dit dat het tijdvak vrij is en krijgt de tekst een groene achtergrond. Anders krijgt de tekst een rode achtergrond;
  • regel 32: we schrijven de naam van de klant voor wie de RV [4] is gereserveerd. Als het [rv==null] is, bestaat deze informatie niet, maar Angular behandelt dit geval correct en geeft geen foutmelding;
  • regels 34-39: hier worden een van de twee knoppen weergegeven: [Réserver] of [Supprimer]. Of er al dan niet een afspraak is, bepaalt welke van de twee knoppen wordt gekozen;

3.7.8.5. Aanpassing van de webserver

Net als bij de voorgaande voorbeelden moet de webserver worden aangepast zodat de URL [/getAgendaMedecinJour] de headers CORS verstuurt:

  

In de klasse [RdvMedecinsCorsController] voegen we een nieuwe methode toe:


    // agenda van de arts
    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
    public void getAgendaMedecinJour(HttpServletResponse response) {
        sendOptions(response);
}

Deze methode verstuurt de headers CORS voor de verzoeken HTTP en [OPTIONS]. We moeten hetzelfde doen voor de verzoeken HTTP en [GET] in de klasse [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
        // kopteksten CORS
        rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}

3.7.8.6. Gebruik van richtlijnen

Net als eerder zullen we de verwerking van DOM in richtlijnen onderbrengen. We hebben twee verwerkingen van DOM:

  • bij de eerste weergave van de weergave:

      // de vervolgkeuzelijst wordt opgemaakt
      $timeout(function () {
        $('.selectpicker').selectpicker();
});
  • bij het weergeven van de agenda:

            // er wordt een event aangemaakt om de tabel te stylen nadat de weergave is getoond
            $timeout(function () {
              $("#creneaux").footable();
});

Voor het eerste geval gebruiken we de reeds besproken richtlijn [selectEnable]. Voor het tweede geval maken we de richtlijn [footable] aan in het volgende bestand JS [footable.js]:


angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive footable");
      $timeout(function () {
        $("#creneaux").footable();
      })
    }
  }
}]);

We gebruiken dus dezelfde techniek als voor de richtlijn [selectEnable].

De code HTML [app-18.html] wordt gedupliceerd in [app-18B.html]. Vervolgens wordt deze als volgt aangepast:


        <select data-style="btn-primary" class="selectpicker" select-enable="">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
</select>
  • regel 1: de richtlijn [selectEnable] (via het attribuut [select-enable]) wordt toegepast op de tag <select> van de artsen;

    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table id="creneaux" class="table" footable="">
          <thead>
<tr>
  • regel 3: de richtlijn [footable] (via het attribuut [footable]) wordt toegepast op de tabel HTML van de agenda;

<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- richtlijnen -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • regels 3-4: er wordt verwezen naar de bestanden JS van beide richtlijnen;
  • regel 1: de code JS van [app-18B.html] is de code JS van [app-18.html], gedupliceerd in het bestand [rdvmedecins-06B.js];

Het bestand [rdvmedecins-06B.js] is identiek aan het bestand [rdvmedecins-06.js], op twee details na. De regels die betrekking hebben op DOM verdwijnen:


      // de vervolgkeuzelijst opmaken
      $timeout(function () {
        $('.selectpicker').selectpicker();
});

            // er wordt een gebeurtenis aangemaakt om de tabel op te maken nadat de weergave is getoond
            $timeout(function () {
              $("#creneaux").footable();
});

Dit betekent dat het uitvoeren van de toepassing [app-18B.html] dezelfde resultaten oplevert als het uitvoeren van [app-18.html].

3.7.9. Voorbeeld 9: reserveringen aanmaken en annuleren

We presenteren nu een applicatie waarmee reserveringen kunnen worden aangemaakt en geannuleerd.

3.7.9.1. Weergave V van de applicatie

We zullen het volgende formulier presenteren:

  • in [1] kunt u reserveren. De reservering die wordt gemaakt, is voor een willekeurige klant;
  • in [2] kunnen we de reserveringen die we hebben gemaakt verwijderen;

We dupliceren het bestand [app-18.html] naar [app-19.html] en passen vervolgens de code als volgt aan:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>

  <!-- de foutenlijst -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>

  <!-- de agenda -->
  <div id="agenda" ng-show="agenda.show">
..
    <!-- agenda van de arts -->
    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table id="creneaux" class="table" footable="">
...
          <tbody>
          <tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
...
            <td>
              <a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active"  ng-click="reserver(creneauMedecin.creneau.id)">
              </a>
              <a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
              </a>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>
....
<script type="text/javascript" src="rdvmedecins-07.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • regels 5-7: het wachtbericht is dat van de vorige versie;
  • regels 10-12: het wachtbericht is dat van de vorige versie;
  • regels 15-36: de agenda is die van de vorige versie, op twee details na:
    • regel 26: het klikken op de knop [réserver] (attribuut ng-click) wordt afgehandeld door de methode [reserver] van het model M van de weergave V. Hieraan wordt het nummer van het reserveringstijdvak doorgegeven;
    • regel 26: het klikken op de knop [supprimer] wordt afgehandeld door de methode [reserver] van het model M van de weergave V. Hieraan wordt het nummer van de te verwijderen afspraak doorgegeven;
  • regel 39: de code JS die de toepassing beheert, staat in het bestand [rdvmedecins-07.js];
  • regel 40: de code JS van de richtlijn [footable] die op regel 20 wordt toegepast;

3.7.9.2. De controller C

De code JS van [rdvmedecins-07.js] wordt eerst verkregen door het bestand [rdvmedecins-06.js] te kopiëren. Vervolgens wordt deze aangepast. We zien nog steeds de gebruikelijke grote codeblokken. De wijzigingen vinden voornamelijk plaats in de controller:

Image

We zullen de C-controller van de weergave V in verschillende stappen beschrijven.

3.7.9.3. Initialisatie van de C-controller

De initialisatiecode van de controller is als volgt:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- initialisatie van het sjabloon
      // sjabloon
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      $scope.errors = {show: false, model: {}};
      $scope.medecins = {
        data: [
          {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
          {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
          {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
          {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
        ],
        title: config.listMedecins
      };
      var médecin = $scope.medecins.data[0];
      var clients = [
        {id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
        {id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
        {id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
        {id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
      ];
      // Franse lokale instelling voor de datum
      angular.copy(config.locales['fr'], $locale);
      var today = new Date();
      var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
      var fullDay = $filter('date')(today, 'fullDate');
      $scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre: médecin.titre, prenom: médecin.prenom, nom: médecin.nom, jour: fullDay}};


      // ---------------------------------------------------------------- initiële agenda
      // de globale asynchrone taak
      var task;
      // de agenda opvragen
      getAgenda();

      // ------------------------------------------------------------------ reservering
      $scope.reserver = function (creneauId) {
....
      };

      // ------------------------------------------------------------ verwijdering RV
      $scope.supprimer = function (idRv) {
...
      };

      // agenda ophalen
      function getAgenda() {
 ...
      }

      // annulering in behandeling
      function cancel() {
...
      }
} ]);
  • regel 6: configuratie van het wachtbericht. Standaard wordt er 3 seconden gewacht voordat een aanroep wordt gedaan HTTP;
  • regel 7: de benodigde informatie voor de oproepen HTTP;
  • regel 8: configuratie van het foutbericht;
  • regels 9-17: de vastgelegde artsen;
  • regel 18: een particuliere arts. Voor zijn spreekuren kunnen reserveringen worden gemaakt;
  • regels 19-24: vaste klanten;
  • regel 26: we willen Franse datums bewerken;
  • regel 27: de afspraken worden gemaakt op de huidige datum;
  • regel 28: de online reserveringsdienst verwacht datums in het formaat 'jjjj-mm-dd';
  • regel 29: de datum van vandaag in de vorm [jeudi 26 juin 2014];
  • regel 30: configuratie van de agenda. Het attribuut [model] bevat de parameters van het geïnternationaliseerde bericht dat zal worden weergegeven:

        agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
  • regel 35: de globale variabele [task] vertegenwoordigt op een bepaald moment de asynchrone taak die op dat moment wordt uitgevoerd;
  • regel 37: de initiële agenda wordt opgevraagd;

Dit is alles wat er gebeurt bij het eerste laden van de pagina. Als alles goed verloopt, toont de weergave de agenda van de dag van mevrouw PELISSIER.

Image

3.7.9.4. De agenda ophalen

De agenda wordt opgehaald met de volgende methode [getAgenda]:


      // agenda opvragen
      function getAgenda() {
        // het pad naar de URL-service
        var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
        // de agenda wordt opgevraagd
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
        // wachtbericht
        $scope.waiting.visible = true;
        // het resultaat van de serviceaanroep wordt geanalyseerd [dao]
        task.promise.then(function (result) {
          // einde wachttijd
          $scope.waiting.visible = false;
          // fout?
          if (result.err == 0) {
            // het agendatemplate wordt voorbereid
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            // opmaak van de weergave van de tijden
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
          } else {
            // er zijn fouten opgetreden bij het ophalen van de agenda
            $scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
          }
        });
}

Deze code is dezelfde als die in de vorige toepassing is behandeld. Er zijn twee wijzigingen:

  • er is geen gesimuleerde wachttijd vóór de aanroep van HTTP;
  • regel 4: er wordt gebruikgemaakt van de arts die bij de initialisatie van de controller is aangemaakt, evenals van de opgemaakte dag die is geconstrueerd;

Deze code is in een functie ondergebracht omdat deze ook wordt gebruikt door de functies [reserver] en [supprimer].

3.7.9.5. Een tijdvak reserveren

Ter herinnering: de klanten worden willekeurig gekozen.

De reserveringscode is als volgt:


$scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        // er wordt een RV aangemaakt met een willekeurige klant in het tijdslot dat wordt aangeduid door [id]
        var idClient = clients[Math.floor(Math.random() * clients.length)].id;
        utils.debug("réservation du créneau pour le client", idClient);
        // gesimuleerde wachttijd
        $scope.waiting.visible = true;
        var task = utils.waitForSomeTime($scope.waiting.time);
        // het tijdvak wordt toegevoegd
        var promise = task.promise.then(function () {
          // het pad naar de URL-service
          var path = config.urlSvrResaAdd;
          // de gegevens die naar de service moeten worden verzonden
          var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
          // de asynchrone taak wordt gestart
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // de voltooiingsbelofte van de taak wordt teruggestuurd
          return task.promise;
        });

        // analyse van het resultaat van de taak
        promise = promise.then(function (result) {
          if (result.err != 0) {
            // er zijn fouten opgetreden bij het valideren van de afspraak
            $scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
          } else {
            // er wordt een nieuwe agenda aangevraagd
            getAgenda();
          }
        });

      };
  • regel 1: ter herinnering: de parameter van de functie [reserver] is het tijdvaknummer (attribuut id);
  • regel 4: er wordt willekeurig een klant gekozen uit de lijst met klanten die vast is gedefinieerd in de initialisatiecode. Van deze klant wordt de identificatiecode [id] onthouden;
  • regels 7-8: de wachttijd van 3 seconden;
  • regels 11-18: deze regels worden pas na afloop van de 3 seconden uitgevoerd;
  • regel 12: de URL van de reserveringsdienst [/ajouterRv]. Deze URL is bijzonder in vergelijking met de exemplaren die we tot nu toe zijn tegengekomen. Hij is als volgt gedefinieerd in de webservice:

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
  • (vervolg)
    • regel 1: de URL heeft geen parameters en wordt aangevraagd met een POST;
    • regel 2: de verzonden parameters worden in de vorm van een JSON-object verzonden. Dit wordt gedeserialiseerd in de parameter [post] (@RequestBody);

We hebben een voorbeeld gezien van dit POST (paragraaf 2.12.2):

  • in [0], de URL van de webservice;
  • in [1] wordt de methode POST gebruikt;
  • in [2], de tekst JSON van de informatie die naar de webservice wordt verzonden in de vorm {dag, idClient, idCreneau};
  • in [3] geeft de client aan de webservice aan dat hij informatie JSON verstuurt;

Laten we teruggaan naar de code JS van de functie [reserver]:

  • regel 14: de te verzenden waarde wordt aangemaakt in de vorm van een object JS. Angular zal dit bij het verzenden serialiseren naar JSON;
  • regel 16: de functie HTTP wordt aangeroepen. De te verzenden waarde is de laatste parameter van de functie [dao.getData]. Wanneer deze parameter aanwezig is, maakt de functie [dao.getData] een POST in plaats van een GET (zie de code in paragraaf 3.7.6.4);
  • regel 18: de belofte van de aanroep HTTP wordt teruggegeven;
  • regels 23-29: worden alleen uitgevoerd wanneer de aanroep HTTP zijn antwoord heeft geretourneerd;
  • regel 23: de parameter [result] heeft de vorm [err,data] of [err,messages], waarbij [err] een foutcode is;
  • regels 23-26: als er fouten zijn opgetreden, wordt de foutmelding weergegeven;
  • regel 28: als de reservering is gelukt, wordt de nieuwe agenda opnieuw weergegeven;

3.7.9.6. Serverwijziging

  

In de klasse [RdvMedecinsCorsController] voegen we de volgende methode toe:


    // de opties worden naar de klant verzonden
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // de header wordt vastgelegd CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // de header wordt goedgekeurd [authorization]
            response.addHeader("Access-Control-Allow-Headers", "authorization");
        }

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
    public void ajouterRv(HttpServletResponse response) {
        sendOptions(response);
}

De toevoeging vindt plaats in de regels 10-13. De headers van de regels 2-8 worden verzonden naar de URL [/ajouterRv] (regel 10) en de methode HTTP [OPTIONS] (regel 10).

Wordt de klasse [RdvMedecinsController] als volgt gewijzigd:


    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
        // headers CORS
        rdvMedecinsCorsController.ajouterRv(response);
...

Voor de methode [POST] (regel 1) en de methoden URL en [/ajouterRv] (regel 1) wordt de methode die we zojuist hebben toegevoegd in [RdvMedecinsCorsController] aangeroepen (regel 4), waardoor dezelfde headers HTTP worden geretourneerd als voor de methoden HTTP en [OPTIONS].

3.7.9.7. Tests

Laten we een eerste test uitvoeren waarbij we een willekeurig tijdvak reserveren:

 

Zoals altijd in dit soort gevallen moeten we de logbestanden van de console bekijken:


[dao] getData[/ajouterRv] error réponse : {"data":"","status":0,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/ajouterRv","data":{"jour":"2014-06-30","idCreneau":1,"idClient":4},"headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4=","Content-Type":"application/json;charset=utf-8"}},"statusText":""}

De methode [dao.getData] is mislukt met [status=0], wat betekent dat Angular het verzoek heeft geannuleerd. De oorzaak van de fout staat in de logbestanden:

XMLHttpRequest cannot load http://localhost:8080/ajouterRv. Het verzoekheaderveld Content-Type is niet toegestaan door Access-Control-Allow-Headers.

Als we naar het netwerkverkeer kijken, zien we het volgende:

  • in [1] en [2]: er was slechts één verzoek, namelijk HTTP; het verzoek [OPTIONS];
  • in [3] vraagt de Angular-client om twee autorisaties:
    • toestemming om de headers HTTP en [accept, authorization, content-type] te verzenden;
    • toestemming om een commando POST te verzenden;
  • in [4]: de server verleent toestemming voor de header [authorization]. Ter herinnering: aan de serverzijde zijn wijzelf degene die deze toestemming verstrekken;

Het nieuwe is dus dat bij een bewerking POST de Angular-client meer autorisaties aan de server vraagt. We moeten de server dus aanpassen zodat deze ze aan de client verstrekt:

  

In de klasse [RdvMedecinsCorsController] passen we de privémethode aan die de HTTP-headers genereert die worden verzonden voor de opdrachten OPTIONS, GET en POST:


    // opties naar de client verzenden
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // de header CORS wordt ingesteld
            response.addHeader("Access-Control-Allow-Origin", "*");
            // bepaalde headers worden toegestaan
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // POST wordt toegestaan
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
}
  • regel 7: er is een autorisatie toegevoegd voor de headers HTTP en [accept, content-type];
  • regel 9: er is een autorisatie toegevoegd voor de methode POST;

We voeren de test opnieuw uit nadat we de server opnieuw hebben opgestart:

 

Deze keer is het gelukt om te reserveren.

3.7.9.8. Een afspraak verwijderen

De functiecode [supprimer] is als volgt:


$scope.supprimer = function (idRv) {
        utils.debug("suppression rv n°", idRv);
        // gesimuleerde wachttijd
        $scope.waiting.visible = true;
        task = utils.waitForSomeTime($scope.waiting.time);
        // het tijdslot wordt toegevoegd
        var promise = task.promise.then(function () {
          // het pad naar de URL-service
          var path = config.urlSvrResaRemove;
          // de gegevens die naar de service moeten worden verzonden
          var post = {idRv: idRv};
          // de asynchrone taak wordt gestart
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // de voltooiingsbelofte van de taak wordt teruggestuurd
          return task.promise;
        });

        // analyse van het resultaat van de taak
        promise = promise.then(function (result) {
          if (result.err != 0) {
            // er zijn fouten opgetreden bij het verwijderen van de rv
            $scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
            // de UI wordt bijgewerkt
            $scope.waiting.visible = false;
          } else {
            // de nieuwe agenda wordt opgevraagd
            getAgenda();
          }
        });
      };
  • regel 1: houd er rekening mee dat de parameter van de functie het nummer is van de afspraak die moet worden verwijderd. Deze code lijkt sterk op die van de reservering. We bespreken hier alleen de verschillen;
  • regel 9: de URL van de service is hier [/supprimerRV] en ook hier wordt deze benaderd via een POST:

    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {

De verzonden parameter wordt hier opnieuw doorgegeven in de vorm JSON. In paragraaf 2.12.17 hebben we de aard van de handmatig gegenereerde POST laten zien:

  • in [1], de URL van de webservice;
  • in [2] wordt de methode POST gebruikt;
  • in [3], de tekst JSON van de informatie die in de vorm {idRv} naar de webservice wordt verzonden;
  • in [4] geeft de client aan de webservice aan dat hij informatie JSON verstuurt;

Laten we teruggaan naar de code JS van de functie [supprimer]:

  • regel 11: we maken het POST-object aan. Angular zal dit automatisch serialiseren naar JSON;

De rest van de code is vergelijkbaar met die van de reservering.

3.7.9.9. Wijzigingen aan de serverzijde

Aan de serverzijde voeren we de volgende wijzigingen door:

  

In de klasse [RdvMedecinsCorsController] voegen we de volgende methode toe:


    // de opties worden naar de klant verzonden
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // de header wordt vastgelegd CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // bepaalde headers worden toegestaan
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // POST wordt toegestaan
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
    }
...
    @RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
    public void supprimerRv(HttpServletResponse response) {
        sendOptions(response);
}

De toevoeging vindt plaats op de regels 13-16. De headers van de regels 2-10 worden verzonden naar de methoden URL en [/supprimerRv] (regel 13) en de methoden HTTP en [OPTIONS] (regel 13).

De klasse [RdvMedecinsController] wordt als volgt gewijzigd:


    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
        // headers CORS
        rdvMedecinsCorsController.supprimerRv(response);
...

Voor de methode [POST] (regel 1) en de methoden URL en [/supprimerRv] (regel 1) wordt de methode die we zojuist hebben toegevoegd in [RdvMedecinsCorsController] aangeroepen (regel 4), waardoor dezelfde headers HTTP worden teruggestuurd als voor de methoden HTTP en [OPTIONS].

3.7.10. Voorbeeld 10: reserveringen aanmaken en annuleren - 2

We presenteren nu dezelfde applicatie als eerder, maar in plaats van te reserveren voor een willekeurige klant, wordt deze geselecteerd uit een vervolgkeuzelijst.

3.7.10.1. Weergave V van de applicatie

We presenteren het volgende formulier:

Klanten worden geselecteerd in [1].

De code is vergelijkbaar met die van de vorige applicatie, dus we laten alleen de belangrijkste verschillen zien.

We dupliceren het bestand [app-19.html] naar [app-20.html] en maken vervolgens de code voor de vervolgkeuzelijst met klanten [1]:


<!-- de klantenlijst -->
  <div class="alert alert-info">
    <h3>{{agenda.title|translate:agenda.model}}</h3>

    <div class="row" ng-show="clients.show">
      <div class="col-md-3">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
  • regels 8-12: de keuzelijst wordt geïmplementeerd met de component [bootstrap-select];
  • regel 1: de richtlijn [selectEnable] wordt toegepast via het attribuut [select-enable];
  • regel 1: de tag <select> wordt alleen gegenereerd als [clients.data] bestaat (# null, undefined). Dit is belangrijk en wordt uitgelegd in paragraaf 3.7.7.8;

Daarnaast importeren we nieuwe bestanden JS:


<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- richtlijnen -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • regel 1: het bestand [rdvmedecins-08.js] wordt verkregen door het bestand [rdvmedecins-0.js] te kopiëren;
  • regels 3-4: de bestanden van beide richtlijnen worden geïmporteerd;

3.7.10.2. De C-controller

De code van de C-controller ontwikkelt zich als volgt:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- initialisatie van het model
...
      // de klanten
      $scope.clients = {title: config.listClients, show: false, model: {}};

      //------------------------------------------- initialisatie van de weergave
      // de globale asynchrone taak
      var task;
      // eerst de klanten opvragen en daarna de agenda
      getClients().then(function () {
        getAgenda();
      });
...

      // actie uitvoeren
      function getClients() {
....
      };
} ]);
  • regel 8: het object [$scope.clients] configureert de vervolgkeuzelijst met klanten in de V-weergave;
  • regels 14-16: op asynchrone wijze wordt eerst de klantenlijst opgevraagd; zodra deze is verkregen, wordt de agenda van mevrouw PELISSIER voor vandaag opgevraagd. De hier gebruikte syntaxis werkt alleen omdat de functie [getClients] een belofte (promise) retourneert;

De methode [getClients] vraagt de lijst met klanten op:


function getClients() {
        // de UI wordt bijgewerkt
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // de lijst met klanten wordt opgevraagd;
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // het resultaat van de vorige aanroep wordt geanalyseerd
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // de verzamelde gegevens worden in het model geplaatst
            $scope.clients.data = result.data;
            // we werken de UI bij
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            // er zijn fouten opgetreden bij het ophalen van de klantenlijst
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // het bestand UI wordt bijgewerkt
            $scope.waiting.visible = false;
          }
        });
        // de belofte wordt nagekomen
        return promise;
      };

Dit is code die we al eerder zijn tegengekomen en hebben besproken. Het belangrijkste om op te merken is regel 31:

  • regel 27: de promise van regel 10 wordt teruggegeven, d.w.z. de laatste promise die in de code is verkregen. Deze promise wordt pas verkregen wanneer de aanroep HTTP zijn antwoord heeft teruggegeven;

De methode [reserver] is enigszins gewijzigd:


      $scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        // er wordt een RV aangemaakt voor de geselecteerde klant
        var idClient = $(".selectpicker").selectpicker('val');
        ...
        });
  • regel 4: er wordt niet langer gereserveerd voor een willekeurige klant, maar voor de klant die uit de klantenlijst is geselecteerd.

3.7.11. Voorbeeld 11: een richtlijn [selectEnable2]

Dit voorbeeld gaat terug op de richtlijnen.

3.7.11.1. Weergave V

De applicatie geeft de volgende weergave weer:

 

3.7.11.2. De code HTML van de weergave

De code HTML van de weergave [app-21.html] is als volgt:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- de foutenlijst -->
  <div class="alert alert-danger" ng-show="errors.show">
   ...
  </div>

  <!-- de klantenlijst -->
  <div class="alert alert-info">
    <div class="row" ng-show="clients.show">
      <div class="col-md-4">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>

  <!-- de lijst met artsen -->
  <div class="alert alert-info">
    <div class="row" ng-show="medecins.show">
      <div class="col-md-4">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-09.js"></script>
<!-- richtlijnen -->
<script type="text/javascript" src="selectEnable2.js"></script>
  • regels 19-23: de vervolgkeuzelijst met klanten;
  • regel 19: de richtlijn [selectEnable2] (attribuut [select-enable2]) wordt toegepast;
  • regel 19: alleen als [clients.data] niet leeg is;
  • regel 19: de vervolgkeuzelijst wordt geïdentificeerd door het attribuut [id="selectpickerClients"];
  • regels 33-37: de vervolgkeuzelijst met artsen;
  • regel 33: de richtlijn [selectEnable2] (attribuut [select-enable2]) wordt toegepast;
  • regel 33: alleen als [medecins.data] niet leeg is;
  • regel 33: de vervolgkeuzelijst wordt geïdentificeerd door het attribuut [id="selectpickerMedecins"];
  • regel 43: er wordt een nieuw bestand JS [rdvmedecins-09.js] geïmporteerd;
  • regel 45: het bestand JS uit de nieuwe richtlijn wordt geïmporteerd;

3.7.11.3. De richtlijn [selectEnable2]

De code van de richtlijn [selectEnable2] is als volgt:


angular.module("rdvmedecins").directive('selectEnable2', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable2 attrs", attrs);
      $timeout(function () {
        $('#' + attrs['id']).selectpicker();
      })
    }
  }
}]);
  • regel 4: we laten de waarde van de parameter [attrs] weergeven om de werking van de code te verduidelijken. We zullen zien dat attrs['id']='selectpickerClients' geldt voor de klantenlijst;
  • regel 6: om in DOM een element van [id='x'] te vinden, schrijven we [$('#x')]. We moeten dus [$('#selectpickerClients')] schrijven om de klantenlijst te vinden. Dit wordt bereikt met de syntaxis [$('#' + attrs['id'])];

De instructie [selectEnable2] maakt dus gebruik van de informatie die wordt meegegeven door een van de attributen van het element HTML waarop deze wordt toegepast.

3.7.11.4. De C-controller

De C-controller bevindt zich in het bestand JS [rdvmedecins-09.js] en heeft de volgende structuur:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- initialisatie van het sjabloon
      // het wachtbericht
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      // de inloggegevens
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      // de fouten
      $scope.errors = {show: false, model: {}};
      // de artsen
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      // de klanten
      $scope.clients = {title: config.listClients, show: false, model: {}};

      // de globale asynchrone taak
      var task;
      // ---------------------------------------------------- weergave initialiseren
      // UI wordt bijgewerkt
      $scope.waiting.visible = true;
      $scope.clients.show = false;
      $scope.medecins.show = false;
      $scope.errors.show = false;
      // de klanten en vervolgens de artsen opvragen
      getClients().then(function () {
        getMedecins();
      });

      // klantenlijst
      function getClients() {
        ...
      }

      // lijst met artsen
      function getMedecins() {
...
      }

      // afzegging in behandeling
      function cancel() {
...
      }
    } ]);
  • regels 26-28: eerst worden de klanten opgevraagd en daarna de artsen;

3.7.11.5. De tests

Test deze nieuwe versie.

3.7.12. Voorbeeld 12: een richtlijn [list]

We nemen hetzelfde voorbeeld als eerder, maar we willen de code HTML vereenvoudigen door een richtlijn te gebruiken. Momenteel hebben we namelijk de volgende code HTML:


<!-- de klantenlijst -->
  <div class="alert alert-info">
    <div class="row" ng-show="clients.show">
      <div class="col-md-4">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
  <!-- de lijst met artsen -->
  <div class="alert alert-info">
    <div class="row" ng-show="medecins.show">
      <div class="col-md-4">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>

De regels 14-26 zijn identiek aan de regels 1-13. Ze hebben betrekking op artsen in plaats van op klanten. We zouden graag het volgende willen kunnen schrijven:


  <!-- de lijst met klanten -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- de lijst met artsen -->
<list model="medecins" ng-if="medecins.show"></list>

Deze code maakt gebruik van een nieuwe richtlijn [list] die we nu gaan aanmaken.

3.7.12.1. De richtlijn [list]

De richtlijn [list] wordt in het bestand JS [list.js] geplaatst. De code ervan is als volgt:


angular.module("rdvmedecins")
  .directive("list", ['utils', '$timeout', function (utils, $timeout) {
    // instantie van de geretourneerde richtlijn
    return {
      // element HTML
      restrict: "E",
      // URL van het fragment
      templateUrl: "list.html",
      // uniek bereik voor elke instantie van de richtlijn
      scope: true,
      // functie voor koppeling met het document
      link: function (scope, element, attrs) {
        utils.debug("directive list attrs", attrs);
        scope.model = scope[attrs['model']];
        utils.debug("directive list model", scope.model);
        $timeout(function () {
          $('#' + scope.model.id).selectpicker();
        })
      }
    }
}]);
  • regel 2: definieert een richtlijn met de naam 'list';
  • regel 6: het attribuut [restrict] bepaalt de manieren waarop de richtlijn kan worden gebruikt. [restrict: "E"] betekent dat de richtlijn [list] kan worden gebruikt als element HTML <list ...>...</list>. [restrict: "A"] betekent dat de richtlijn [list] als attribuut kan worden gebruikt, bijvoorbeeld <div ... list='...'>. [restrict: "AE"] betekent dat de richtlijn [list] zowel als attribuut als element kan worden gebruikt;
  • regel 8: het attribuut [templateUrl] geeft de naam aan van het fragment HTML dat moet worden gebruikt wanneer de tag wordt aangetroffen. Dit fragment vormt de inhoud van de tag;
  • regel 10: het attribuut [scope] bepaalt het bereik van het sjabloon van de richtlijn. [scope: true] betekent dat twee elementen van het type <list> elk hun eigen sjabloon zullen hebben. Standaard (bereik niet geïnitialiseerd) delen ze hun sjablonen;
  • regel 12: de functie [link] die we al meerdere keren hebben gebruikt;

Om de bovenstaande code te begrijpen, moet je in gedachten houden waarvoor de richtlijn wordt gebruikt:


  <!-- de lijst met klanten -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- de lijst met artsen -->
<list model="medecins" ng-if="medecins.show"></list>

De richtlijn [list] wordt gebruikt als het element HTML <list>. Dit element heeft twee attributen:

  • [model]: dit attribuut krijgt als waarde het element uit model M van weergave V waarin de richtlijn [list] zich bevindt. Dit element vult het model van de richtlijn aan;
  • [ng-if]: dit zorgt ervoor dat de code HTML van de richtlijn niet wordt gegenereerd als er niets te weergeven is;

Laten we teruggaan naar de code van de functie [link] van de richtlijn:


link: function (scope, element, attrs) {
        utils.debug("directive list attrs", attrs);
        scope.model = scope[attrs['model']];
        utils.debug("directive list model", scope.model);
        $timeout(function () {
          $('#' + scope.model.id).selectpicker();
        })
      }

Laten we deze code JS koppelen aan de code HTML die de richtlijn gebruikt:


  <list model="clients" ng-if="clients.show"></list>
  • regel 3: attrs['model'] heeft hier de waarde 'clients';
  • regel 3: scope[attrs['model']] heeft de waarde scope['clients'] en vertegenwoordigt dus [$scope.clients], d.w.z. het veld [clients] van het weergavemodel. Dit veld krijgt de waarde {id:'...', data:[client1, client2, ...], show: ..., title:'...'};
  • regel 3: we voegen een veld [model] toe aan het model van de richtlijn. Deze heeft het model geërfd van de weergave waarin hij zich bevindt. We moeten dus conflicten vermijden met een eventueel veld [model] dat de weergave mogelijk ook zou kunnen hebben. Hier zal er geen conflict zijn;
  • regel 4: we geven [scope.model] weer om de code beter te begrijpen;
  • regels 5-7: we zien hier code die we al eerder zijn tegengekomen. Het verschil is dat de id van de component eerder in een attribuut attrs['id'] stond. Hier staat hij in [scope.model.id];

Laten we nu eens kijken naar de code HTML die door de richtlijn wordt gegenereerd. Vanwege het attribuut [templateUrl: "list.html"] van de richtlijn moet deze worden opgezocht in het bestand [list.html]:


<!-- een lijst met klanten of artsen -->
<div class="alert alert-info" ng-show="model.show">
  <div class="row">
    <div class="col-md-4">
      <h2 translate="{{model.title}}"></h2>
      <select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
        <option ng-repeat="element in model.data" value="{{element.id}}">
          {{element.titre}} {{element.prenom}} {{element.nom}}
        </option>
      </select>
    </div>
  </div>
</div>
  • Het eerste wat je moet onthouden om deze code te lezen, is dat de richtlijn een object [scope.model] heeft aangemaakt in de vorm [{id :'...', data:[client1, client2, ...], show : ..., title :'...'}]. Dit object [model] (scope is impliciet in de code HTML) wordt gebruikt door de code HTML van de richtlijn;
  • regel 2: gebruik van [model.show] om de door de richtlijn gegenereerde weergave te tonen/verbergen;
  • regel 5: gebruik van [model.title] om een titel in te voeren;
  • regel 6: gebruik van [model.id] om een id toe te voegen aan de tag <select>. Deze id wordt gebruikt door de code JS van de richtlijn;
  • regel 6: gebruik van [model.data] om de <select> alleen te genereren als er gegevens moeten worden weergegeven;
  • regels 7-9: gebruik van [model.data] om de elementen van de vervolgkeuzelijst te genereren;

3.7.12.2. De code HTML

De code HTML van de applicatie [app-22.html] is als volgt:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- de foutenlijst -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>

  <!-- de lijst met klanten -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- de lijst met artsen -->
  <list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- richtlijnen -->
<script type="text/javascript" src="list.js"></script>
  • regel 22: vergeet niet de code JS van de richtlijn op te nemen;

3.7.12.3. De C-controller

De C-controller verandert nauwelijks:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- initialisatie van het sjabloon
...
      // de artsen
      $scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
      // de klanten
      $scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
  • regels 7 en 9 voegen we het attribuut [id] toe aan de sjablonen voor artsen en klanten;

3.7.12.4. De tests

De tests leveren dezelfde resultaten op als in het vorige voorbeeld.

3.7.13. Voorbeeld 13: het sjabloon van een richtlijn bijwerken

We blijven bij het bestuderen van richtlijnen en houden vast aan het voorbeeld van de vervolgkeuzelijst. We willen hier het gedrag van de richtlijn [list] onderzoeken wanneer de inhoud van de vervolgkeuzelijst verandert.

3.7.13.1. De V-weergaven

De verschillende weergaven zijn als volgt:

  • in [1] wordt de klantenlijst voor de eerste keer opgevraagd;
  • in [2] wordt de klantenlijst een tweede keer opgevraagd. Deze tweede lijst wordt vervolgens samengevoegd met de eerste, [3]. In dit voorbeeld willen we de update van de component [Bootstrap select] bekijken.

3.7.13.2. De pagina HTML

De pagina HTML [app-23.html] wordt verkregen door [app-22.html] te kopiëren en vervolgens als volgt te wijzigen:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- de foutenlijst -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>

  <!-- de knop -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- de klantenlijst -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- richtlijnen -->
<script type="text/javascript" src="list2.js"></script>

De wijzigingen ten opzichte van de vorige toepassing zijn als volgt:

  • regels 15-17: toevoeging van een knop;
  • regel 20: gebruik van een nieuwe richtlijn [list2];
  • regel 23: gebruik van een nieuw bestand JS;
  • regel 25: het bestand JS wordt geïmporteerd vanuit de richtlijn [list2];

3.7.13.3. De richtlijn [list2]

De richtlijn [list2] in [list2.js] luidt als volgt:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    // teruggegeven instantie van de richtlijn
    return {
      // element HTML
      restrict: "E",
      // URL van het fragment
      templateUrl: "list.html",
      // uniek bereik voor elke instantie van de richtlijn
      scope: true,
      // functie voor koppeling met het document
      link: function (scope, element, attrs) {
        utils.debug('directive list2');
        scope.model = scope[attrs['model']];
        $timeout(function () {
          $('#' + scope.model.id).selectpicker('refresh');
        })
      }
    }
}]);

Het enige verschil met de richtlijn [list] is regel 16: met de methode [selectpicker('refresh')] wordt de component [Bootstrap-select] gevraagd om te verversen. Het idee hierachter is dat telkens wanneer de gebruiker een nieuwe klantenlijst opvraagt, de vervolgkeuzelijst wordt vernieuwd. Dit zal niet werken, maar het is het basisidee.

3.7.13.4. De C-controller

De controller bevindt zich in het bestand [rdvmedecins-11.js], dat is gekopieerd van het bestand [rdvmedecins-10.js]:


      // de klanten
      $scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
      // lijst met klanten
      $scope.getClients = function getClients() {
        // UI wordt bijgewerkt
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        // de klantenlijst wordt opgevraagd;
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // het resultaat van de vorige aanroep wordt geanalyseerd
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
             // de verkregen gegevens worden in een nieuw model geplaatst om de weergave te vernieuwen
            $scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
            // we werken de UI bij
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            // er zijn fouten opgetreden bij het ophalen van de klantenlijst
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // het UI wordt bijgewerkt
            $scope.waiting.visible = false;
          }
        });
}
  • regel 1: om het samenvoegen van tabellen in [clients.data] mogelijk te maken, wordt dit object geïnitialiseerd met een lege tabel;
  • regel 18: de nieuwe klantenlijst wordt samengevoegd met de lijsten die al in de array [clients.data] staan;

Voorheen hadden we geschreven:

// de verkregen gegevens worden in het sjabloon geplaatst
$scope.clients.data = result.data;

Nu schrijven we:

// de verkregen gegevens worden in een nieuw sjabloon geplaatst om de weergave te verversen
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

Om deze code te begrijpen, moet je je herinneren hoe het model M wordt gebruikt in de weergave V in het geval van de richtlijn [list2]:


  <!-- de klantenlijst -->
<list2 model="clients" ng-if="clients.show"></list2>

Het model dat door de richtlijn [list2] wordt gebruikt, is [clients]. Deze wordt in de V-weergave alleen opnieuw geëvalueerd als [clients] in het M-model van de weergave verandert. Het eerste idee dat bij een wijziging opkomt, is om het volgende te schrijven:

$scope.clients.data=$scope.clients.data.concat(result.data) ;

om er rekening mee te houden dat de nieuwe klantenlijst aan de vorige moet worden toegevoegd. Hierdoor wordt [clients.data] gewijzigd, maar niet [clients]. Ik ben niet bekend met de fijne kneepjes van JavaScript, maar het zou me niet verbazen als [clients] een pointer is, net als [clients.data]. De pointer [clients] verandert niet wanneer de pointer [clients.data] wordt gewijzigd. De instructie [list2] wordt dan niet opnieuw geëvalueerd. Dit is inderdaad wat we zien bij het debuggen van de applicatie (F12 in Chrome).

Door het volgende te schrijven:

$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

Zorgen we ervoor dat [$scope.clients] inderdaad een nieuwe waarde krijgt. De pointer [$scope.clients] verwijst naar een nieuw object. De instructie [list2] zou dan opnieuw moeten worden geëvalueerd. Toch krijgen we niet het gewenste resultaat. Laten we de schermafbeeldingen bekijken wanneer we twee keer de klantenlijst opvragen:

  • in [1] zijn er slechts vier elementen in plaats van acht;
  • in [2] staan deze vier elementen in een [select], maar deze is verborgen (style='display: none');
  • in [3] bevinden de vier klanten zich in een andere structuur, HTML, en dit is wat de gebruiker ziet wanneer hij op de vervolgkeuzelijst klikt;

Ten slotte geven de consolelogbestanden het volgende weer:

1
2
3
4
[dao] init
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
directive list2
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
  • regel 1: de service [dao] wordt geïnstantieerd;
  • regel 2: de service [dao] ontvangt een eerste lijst met clients;
  • regel 3: de richtlijn [list2] wordt uitgevoerd;
  • regel 4: de service [dao] haalt een tweede lijst met klanten op;

De weergave van regel 2 is afkomstig van de volgende code in de instructie:


      link: function (scope, element, attrs) {
        utils.debug('directive list2');
        ...
}

Laten we de levenscyclus van de richtlijn [list2] eens bekijken:

  • tussen regel 1 en 2 is deze niet geactiveerd, terwijl de weergave al een eerste keer is weergegeven. Dit komt door het attribuut [ng-if="clients.show"] in weergave V:

<list2 model="clients" ng-if="clients.show"></list2>
  • regel 3: nadat de eerste lijst met artsen is opgehaald, wordt [clients.show] op true gezet en wordt de richtlijn geactiveerd;
  • nadat de tweede lijst met klanten is opgehaald, zien we dat de code van de richtlijn [list2] niet wordt aangeroepen. Daarom wordt de tweede lijst niet weergegeven;

Om dit probleem op te lossen, passen we de richtlijn [list2] als volgt aan:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    // instantie van de geretourneerde richtlijn
    return {
      // element HTML
      restrict: "E",
      // URL van het fragment
      templateUrl: "list.html",
      // uniek bereik voor elke instantie van de richtlijn
      scope: true,
      // functie die een koppeling met het document vormt
      link: function (scope, element, attrs) {
        // telkens wanneer attrs["model"] verandert, moet ook het sjabloon van de richtlijn veranderen
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("directive list2 newValue", newValue);
          // het sjabloon van de richtlijn wordt bijgewerkt
          scope.model = newValue;
          $timeout(function () {
            $('#' + scope.model.id).selectpicker('refresh');
          })
        });
      }
    }
}]);
  • regel 14: met de functie [scope.$watch] kan een waarde van het model worden bekeken. De syntaxis is [scope.$watch('var'), f], waarbij [var] de identificatiecode is van een variabele in het model en f de functie die moet worden uitgevoerd wanneer de waarde van deze variabele verandert. Hier willen we de variabele [clients] observeren. We moeten dus [scope.$watch('clients')] schrijven. Aangezien we attrs['model']='clients' hebben, schrijven we [scope.$watch(attrs["model"], function (newValue)] ;
  • regel 14: de tweede parameter van de functie [scope.$watch] is de functie die moet worden uitgevoerd wanneer de waargenomen variabele van waarde verandert. De parameter [newValue] is de nieuwe waarde van de variabele, dus voor ons de nieuwe waarde van de variabele [clients] van het model;
  • regel 17: deze nieuwe waarde wordt toegewezen aan het veld [model] van het richtlijnmodel;

Nadat deze wijziging is doorgevoerd, veranderen de logs:

Hierboven zien we dat, nadat de tweede klantenlijst is verkregen, de richtlijn [list2] inderdaad opnieuw wordt uitgevoerd, wat wordt bevestigd door het resultaat [2].

3.7.14. Voorbeeld 14: de richtlijnen [waiting] en [errors]

Laten we teruggaan naar de code HTML uit de vorige toepassing:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>

  <!-- de foutenlijst -->
  <div class="alert alert-danger" ng-show="errors.show">
  ...
  </div>

  <!-- de knop -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- de klantenlijst -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
  • regels 5-7: het wachtbericht;
  • regels 10-12: het foutbericht;

We besluiten de codes HTML van deze twee berichten in richtlijnen op te nemen.

3.7.14.1. De nieuwe code HTML

De nieuwe code HTML [app-24.html] is als volgt:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- het wachtbericht -->
  <waiting model="waiting"></waiting>

  <!-- de foutenlijst -->
  <errors model="errors"></errors>

  <!-- de knop -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- de klantenlijst -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- richtlijnen -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
  • regel 5: de richtlijn voor het wachtbericht;
  • regel 8: de richtlijn voor het foutbericht;
  • regel 19: het nieuwe bestand JS dat aan de toepassing is gekoppeld;
  • regels 21-23: de bestanden JS van de drie instructies;

3.7.14.2. De instructie [waiting]

De code JS van de richtlijn [waiting] staat in het volgende bestand [waiting.js]:


angular.module("rdvmedecins")
  .directive("waiting", ['utils', function (utils) {
    // de teruggegeven instantie van de richtlijn
    return {
      // element HTML
      restrict: "E",
      // URL van het fragment
      templateUrl: "waiting.html",
      // uniek bereik voor elke instantie van de richtlijn
      scope: true,
      // functie die een koppeling met het document vormt
      link: function (scope, element, attrs) {
        // telkens wanneer attr["model"] verandert, moet het paginasjabloon ook veranderen
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[waiting] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
  }]);

Deze code volgt dezelfde logica als die van de reeds besproken richtlijn [list2].

Op regel 8 wordt verwezen naar het volgende bestand [waiting.html]:


<div class="alert alert-warning" ng-show="model.show">
  <h1>{{ model.title.text | translate:model.title.values}}
    <button class="btn btn-primary pull-right" ng-click="model.cancel()">{{'cancel'|translate}}</button>
    <img src="assets/images/waiting.gif" alt=""/>
  </h1>
</div>

In de code JS van de applicatie wordt het sjabloon [$scope.waiting] van deze code HTML als volgt gedefinieerd:


// het wachtbericht
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};

3.7.14.3. De richtlijn [errors]

De code JS van de richtlijn [errors] staat in het volgende bestand [errors.js]:


angular.module("rdvmedecins")
  .directive("errors", ['utils', function (utils) {
    // instantie van de geretourneerde richtlijn
    return {
      // element HTML
      restrict: "E",
      // URL van het fragment
      templateUrl: "errors.html",
      // uniek bereik voor elke instantie van de richtlijn
      scope: true,
      // functie die een koppeling met het document vormt
      link: function (scope, element, attrs) {
        // telkens wanneer attr["model"] verandert, moet het paginasjabloon ook veranderen
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[errors] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
}]);

Deze code volgt dezelfde logica als die van de reeds besproken richtlijn [list2].

Op regel 8 wordt verwezen naar het volgende bestand [errors.html]:


<div class="alert alert-danger" ng-show="model.show">
  {{model.title.text|translate:model.title.values}}
  <ul>
    <li ng-repeat="message in model.messages">{{message|translate}}</li>
  </ul>
</div>

In de code JS van de applicatie wordt het sjabloon [$scope.errors] van deze code HTML als volgt gedefinieerd:


// er zijn fouten opgetreden bij het ophalen van de klantenlijst
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};

3.7.15. Voorbeeld 15: navigatie

Tot nu toe hebben we applicaties met één pagina gebruikt. In dit voorbeeld gaan we in op applicaties met meerdere pagina’s en de navigatie daartussen.

3.7.15.1. De V-weergaven van de applicatie

  • in [1], de URL van weergave nr. 1;
  • in [2], de inhoud ervan;
  • in [3] gaat men naar pagina 2;
  • in [4], weergave nr. 2;
  • in [5], gaat men naar pagina 3;
  • in [6], weergave nr. 3;
  • in [7] gaat men naar pagina 1;
  • in [8], zijn we terug bij weergave nr. 1;

3.7.15.2. Structuur van de code

We beginnen met een nieuwe indeling van de code:

  
  • de weergaven van de applicatie worden in de map [views] geplaatst;
  • de applicatiemodule wordt in de map [modules] geplaatst;
  • de controllers van de applicatie worden in de map [controllers] geplaatst;

Evenzo worden in de definitieve versie:

  • worden de services in de map [services] geplaatst;
  • de richtlijnen worden in de map [directives] geplaatst;

3.7.15.3. De container met weergaven

De weergaven uit de map [views] worden weergegeven in de volgende container [app-25.html]:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
</head>
<body>
    <div class="container" ng-controller="mainCtrl">
        <!-- de navigatiebalk -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!-- het huidige scherm -->
        <ng-view></ng-view>
    </div>

...
<!-- de module -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- de controllers -->
<script type="text/javascript" src="controllers/mainController.js"></script>
<script type="text/javascript" src="controllers/page1Controller.js"></script>
<script type="text/javascript" src="controllers/page2Controller.js"></script>
<script type="text/javascript" src="controllers/page3Controller.js"></script>
</body>
</html>
  • regel 7: de inhoud van de container wordt beheerd door [mainCtrl];
  • regel 9: met de richtlijn [ng-include] kan een extern bestand HTML worden opgenomen, in dit geval een navigatiebalk;
  • regel 12: de verschillende weergaven die door de container worden getoond, worden weergegeven binnen de instructie [ng-view]. Uiteindelijk hebben we een container die:
    • altijd dezelfde navigatiebalk (regel 9);
    • verschillende weergaven in regel 12;
  • regels 16-22: we importeren de bestanden JS van de applicatiemodule [rdvmedecins-13.js] en de bijbehorende controllers;

3.7.15.4. De applicatiemodule

Het bestand [rdvmedecins-13.js] definieert de applicatiemodule en de navigatie tussen weergaven:


// --------------------- Angular-module
angular.module("rdvmedecins", [ 'ngRoute' ]);

angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ routering
  $routeProvider.when("/page1",
    {
      templateUrl: "views/page1.html",
      controller: 'page1Ctrl'
    });
  $routeProvider.when("/page2",
    {
      templateUrl: "views/page2.html",
      controller: 'page2Ctrl'
    });
  $routeProvider.when("/page3",
    {
      templateUrl: "views/page3.html",
      controller: 'page3Ctrl'
    });
  $routeProvider.otherwise(
    {
      redirectTo: "/page1"
    });
}]);
  • regel 1: hier wordt de module [rdvmedecins] gedefinieerd. Deze is afhankelijk van de module [ngRoute], die wordt geleverd door de bibliotheek [angular-route.min.js]. Deze module maakt de in de regels 6-24 gedefinieerde routing mogelijk;
  • regel 4: definieert de functie [config] van de module [rdvmedecins]. Ter herinnering: deze functie wordt uitgevoerd vóór elke instantiëring van een service. Het is een configuratiefunctie van de module. Hier wordt de routing ervan geconfigureerd. Dit gebeurt met behulp van het object [$routeProvider], geleverd door de module [ngRoute];
  • regels 6-10: definiëren de weergave die moet worden getoond wanneer de gebruiker de URL [/page1] opvraagt. Dit is een interne routering binnen de applicatie. Het URL is in feite [/rdvmedecins-angular-v1/app-21.html#/page1]. We zien dat nog steeds de URL van de container [/rdvmedecins-angular-v1/app-21.html] wordt gebruikt, maar met aanvullende informatie achter een #-teken. Het is deze aanvullende informatie die de Angular-routering verwerkt;
  • regel 8: geeft het fragment HTML aan dat moet worden ingevoegd in de richtlijn [ng-view] van de container:
  • regel 9: geeft de naam van de controller van dit fragment aan;
  • regels 11-15: definiëren de weergave die moet worden getoond wanneer de gebruiker het fragment URL [/page2] opvraagt;
  • regels 16-20: definiëren de weergave die moet worden getoond wanneer de gebruiker de URL [/page3] opvraagt;
  • regels 21-24: definiëren de routering die moet worden toegepast wanneer de opgevraagde URL niet een van de drie voorgaande is (otherwise, regel 21);
  • regel 23: omleiding naar de URL [/page1], dus naar de weergave die is gedefinieerd in de regels 6-10;

3.7.15.5. De controller van de weergavecontainer

We hebben gezien dat de weergavecontainer een controller declareerde:


<div class="container" ng-controller="mainCtrl">

De controller [mainCtrl] is gedefinieerd in het bestand [mainController.js]:


// controller
angular.module("rdvmedecins")
  .controller('mainCtrl', ['$scope', '$location',
    function ($scope, $location) {

      // paginamodellen
      $scope.page1 = {};
      $scope.page2 = {};
      $scope.page3 = {};
      // algemeen sjabloon
      var main = $scope.main = {};
      main.text = "[Modèle global]";

      // methoden die beschikbaar zijn voor de weergave
      main.showPage1 = function () {
        $location.path("/page1");
      };
      main.showPage2 = function () {
        $location.path("/page2");
      };
      main.showPage3 = function () {
        $location.path("/page3");
      }
}]);
  • regel 3: de controller [mainCtrl] heeft het object [$location] nodig, dat wordt geleverd door de routeringsmodule [ngRoute]. Met dit object kan van weergave worden gewisseld (regels 16, 19, 22);

Laten we teruggaan naar de code van de container:


    <div class="container" ng-controller="mainCtrl">
        <!-- de navigatiebalk -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!-- de huidige weergave -->
        <ng-view></ng-view>
</div>
  • de controller [mainCtrl] bouwt het model van zone 1-7 op;
  • de in regel 6 opgenomen weergave heeft ook een controller. De weergave [page1] heeft bijvoorbeeld de controller [page1Ctrl]. Deze bouwt het model van het in regel 6 weergegeven gebied op. In dit gebied zijn er dan twee modellen:
    • het model dat wordt opgebouwd door de controller [mainCtrl];
    • het model dat is opgebouwd door de controller [page1Ctrl];

Er is sprake van een erfenis van modellen. In het weergegeven scherm op regel 6 zijn de modellen van de controllers [mainCtrl] en [pagexCtrl] beide zichtbaar. Als twee variabelen in deze modellen dezelfde naam hebben, zal de ene de andere verbergen. Om deze naamconflict te voorkomen, maken we vier modellen aan met vier verschillende namen:

page
contrôleur
modèle
ligne du code
container
mainCtrl
hand
11
pagina 1
page1Ctrl
pagina 1
7
pagina 2
page2Ctrl
pagina 2
8
pagina 3
page3Ctrl
pagina 3
9
  • regel 12: definieert een element [text] in het model [main];

De regels 7-11 hebben een heel bijzonder gevolg: ze definiëren de [$scope] van de controller [mainCtrl] en creëren daarin vier variabelen [main, page1, page2, page3]. Deze vier variabelen worden gebruikt als respectievelijke sjablonen voor de container en de drie weergaven die deze achtereenvolgens zal bevatten.

3.7.15.6. De navigatiebalk

De navigatiebalk wordt als volgt in de container gedefinieerd:


    <div class="container" ng-controller="mainCtrl">
        <!-- de navigatiebalk -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!-- de huidige weergave -->
        <ng-view></ng-view>
</div>

De navigatiebalk wordt gedefinieerd op regel 3. Dit betekent dat deze alleen het sjabloon [main] kent. De code ervan is als volgt:


<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">RdvMedecins</a>
    </div>
    <div class="collapse navbar-collapse">
      <ul class="nav navbar-nav">
        <li class="active">
          <a href="">
            <span ng-click="main.showPage1()">Page 1</span>
          </a>
        </li>
        <li class="active">
          <a href="">
            <span ng-click="main.showPage2()">Page 2</span>
          </a>
        </li>
        <li class="active">
          <a href="">
            <span ng-click="main.showPage3()">Page 3</span>
          </a>
        </li>
      </ul>
    </div>
  </div>
</div>
  • op de regels 16, 21 en 26 worden methoden van het model [main] gebruikt;
  • regel 16: een klik op de link [Page1] start de uitvoering van de methode [$scope.main.showPage1]. Deze is in de controller [mainCtrl] als volgt gedefinieerd:

      // algemeen model
      var main = $scope.main = {};
      main.text = "[Modèle global]";

      // methoden die aan de weergave worden blootgesteld
      main.showPage1 = function () {
        $location.path("/page1");
};
  • regel 6: uit de bovenstaande code blijkt dat de methode [main.showPage1] in werkelijkheid de methode [$scope.main.showPage1] is. Het is dus inderdaad deze methode die zal worden uitgevoerd;
  • regel 7: we wijzigen de URL van de applicatie in [/page1]. Laten we teruggaan naar de routing die in de hoofdmodule is gedefinieerd:

  $routeProvider.when("/page1",
    {
      templateUrl: "views/page1.html",
      controller: 'page1Ctrl'
});

we zien dat het fragment [views/page1.html] in de container wordt ingevoegd en dat de controller ervan [page1Ctrl] is.

3.7.15.7. De weergave [/page1] en de bijbehorende controller

Het fragment [views/page1.html] is als volgt:


<h1>Page 1</h1>
<div class="alert alert-info">
  <ul>
    <li>Modèle global : {{main.text}}</li>
    <li>Modèle local : {{page1.text}}</li>
  </ul>
</div>

We herinneren ons dat in de weergave die in de container is ingevoegd, het model [main] zichtbaar is. Dit willen we in regel 4 controleren. Bovendien definieert de controller [page1Ctrl] van het fragment [views/page1.html] een model [page1]. Dit model wordt in regel 5 gebruikt.

De code van de controller [page1Ctrl] is als volgt:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope',
    function ($scope) {

      // paginamodel 1
      var page1=$scope.page1;
      page1.text="[Modèle local dans page 1]";
}]);
  • regel 2: de hier ingevoegde [$scope] is niet leeg. Aangezien de controller [page1Ctrl] een zone controleert die is ingevoegd in een container die wordt gecontroleerd door [mainCtrl], bevat de [$scope] uit regel 2 de elementen van de [$scope] die is gedefinieerd door de controller [mainCtrl]. Het is belangrijk om dit te begrijpen. De [$scope], gedefinieerd door de controller [mainCtrl], bevat de volgende elementen: [main, page1, page2, page3]. Dit betekent dat we toegang hebben tot de modellen van alle weergaven. Dit is niet per se wenselijk, maar in dit geval is het wel zo. In de definitieve versie van de Angular-client zullen we deze eigenschap gebruiken om in het model [main] de informatie op te slaan die tussen views moet worden gedeeld. Dit is vergelijkbaar met het concept van een ‘sessie’ aan de serverzijde;
  • regel 6: we halen in [$scope] het model [page1] van pagina 1 op en werken vervolgens daarmee (regel 7). We krijgen dan de volgende weergave:
 

De weergaven [/page2] en [/page3] zijn opgebouwd volgens hetzelfde sjabloon als de weergave [/page1] (zie de schermafbeeldingen op pagina 240).

3.7.15.8. Navigatie controleren

We willen nu de navigatie als volgt regelen: [page1 --> page2 --> page3 --> page1]. Als de gebruiker zich dus op pagina 1 ([/page1]) bevindt en in zijn browser URL of [/page3] invoert, mag deze navigatie niet worden geaccepteerd en moet men op pagina 1 blijven.

Om dit resultaat te bereiken, passen we de paginacontrollers als volgt aan:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope', '$location',
    function ($scope, $location) {
      // navigatie toegestaan?
      var main = $scope.main;
      if (main.lastUrl && main.lastUrl != '/page3') {
        // terug naar de laatste URL
        $location.path(main.lastUrl);
        return;
      }
      // de URL van de pagina wordt opgeslagen
      main.lastUrl = '/page1';
      // paginatemplate
      var page1 = $scope.page1;
      page1.text = "[Modèle local dans page 1]";
    }]);
  • regel 12: wanneer een pagina wordt weergegeven, slaan we de URL ervan op in het model [main.lastUrl]. We passen hier het eerder besproken concept toe: het gebruik van het sjabloon [main] om informatie op te slaan die door alle weergaven wordt gedeeld. In dit geval is dat de laatst geraadpleegde URL;
  • de code van de regels 4-12 wordt gedupliceerd en aangepast aan de drie weergaven. We bevinden ons hier in de weergave [/page1];
  • regel 5: we halen het model [main] op;
  • regel 6: als het sjabloon [main.lastUrl] bestaat en verschilt van [/page3], dan is navigatie niet toegestaan (het laatst bezochte URL bestaat en is niet /page3);
  • regel 8: we keren dan terug naar de laatst bezochte URL;

Laten we het eens proberen:

  • in [1] bevinden we ons op pagina 1 en voeren we de URL van pagina 3 in als [2];
  • bij [3] heeft de navigatie niet plaatsgevonden en zijn we teruggekeerd naar URL op pagina 1;

3.7.16. Conclusie

We hebben alle gebruiksscenario’s doorgenomen die we in de definitieve versie van de Angular-client zullen tegenkomen. Wanneer we deze presenteren, zullen we meer ingaan op de functionaliteiten van de applicatie dan op de implementatiedetails. Voor deze laatste zullen we volstaan met een verwijzing naar het voorbeeld dat het betreffende gebruiksscenario illustreert.

3.8. De definitieve Angular-client

3.8.1. Projectstructuur

Het uiteindelijke project ziet er als volgt uit:

  • in [1], het volledige project. [app.html] is de hoofdpagina van de applicatie;
  • in [2], de controllers;
  • in [3], de directives;
  • in [4], de services en de Angular-module [main.js] van de applicatie;
  • in [5], de verschillende weergaven die in de hoofdpagina [app.html] worden ingevoegd;

3.8.2. De afhankelijkheden van het project

De afhankelijkheden van het project zijn als volgt:

 

De rol van deze verschillende elementen is uitgelegd in paragraaf 3.4, pagina 134.

3.8.3. De masterpagina [app.html]

De masterpagina is als volgt:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  <title>RdvMedecins</title>
  <!-- META -->
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Angular client for RdvMedecins">
  <meta name="author" content="Serge Tahé">
  <!-- de CSS -->
  <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
  <link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
  <link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
  <link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
  <link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!-- controller [appCtrl], sjabloon [app] -->
<body ng-controller="appCtrl">
<div class="container">
 ...
</div>
<!-- Bootstrap-kern JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- modules -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- services -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- richtlijnen -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- controllers -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
  • regel 18: let op: [appCtrl] is de controller van de masterpagina;
  • regels 19-21: de inhoud van de masterpagina;

Deze inhoud is als volgt:


<div class="container">
  <!-- navigatiebalken -->
  <ng-include src="'views/navbar-start.html'" ng-show="app.navbarstart.show"></ng-include>
  <ng-include src="'views/navbar-run.html'" ng-show="app.navbarrun.show"></ng-include>
  <!-- het jumbotron -->
  <ng-include src="'views/jumbotron.html'"></ng-include>
  <!-- de paginatitel -->
  <div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
       translate-values="{{app.titre.model}}"></div>
  <!-- paginafouten -->
  <errors model="app.errors" ng-show="app.errors.show"></errors>
  <!-- het wachtbericht -->
  <waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
  <!-- de huidige weergave -->
  <ng-view></ng-view>
  <!-- debug -->
  <debug model="app" ng-show="app.debug.on"></debug>
</div>

Ongeacht de weergegeven weergave bevat deze altijd de volgende elementen:

  • regels 3-4: een bedieningsbalk. De twee balken op regel 3 en 4 sluiten elkaar uit;

Image

Image

  • regel 6: een logo / tekst van de applicatie:

Image

  • regel 8: een titel

Image

  • regel 11: een foutmelding:

Image

  • regel 13: een wachtmelding:

Image

  • regel 17: debug-informatie:

Image

Alle voorgaande elementen worden beheerd door een richtlijn [ng-show / ng-hide], waardoor ze, ook al zijn ze aanwezig, niet per se zichtbaar zijn.

3.8.4. De weergaven van de applicatie

In de code van de masterpagina staat:


<div class="container">
  ...
  <!-- de huidige weergave -->
  <ng-view></ng-view>
  ...
</div>

Regel 4 bevat de verschillende weergaven van de applicatie. Deze zijn gedefinieerd in de module [main.js]:

Image

De rol van de configuratie van de verschillende routes is uitgelegd in paragraaf 3.7.15.4, pagina 242.

De weergave [login.html] is leeg, d.w.z. dat er geen elementen aan worden toegevoegd aan de elementen die al op de masterpagina staan.

De weergave [home.html] voegt het volgende element toe aan de masterpagina:

Image

De weergave [agenda.html] voegt het volgende element toe aan de masterpagina:

Image

De weergave [resa.html] voegt het volgende element toe aan de masterpagina:

Image

3.8.5. Functies van de applicatie

De weergaven van de Angular-client zijn al besproken in paragraaf 1.3.3, pagina 7. Om het lezen van dit nieuwe hoofdstuk te vergemakkelijken, geven we ze hier nogmaals weer. De eerste weergave is de volgende:

  • in [6], de startpagina van de applicatie. Dit is een applicatie voor het maken van afspraken bij artsen;
  • in [7], een selectievakje waarmee je al dan niet in de modus [debug] kunt zijn. Deze laatste wordt gekenmerkt door de aanwezigheid van het kader [8] dat het sjabloon van de huidige weergave weergeeft;
  • in [9], een kunstmatige wachttijd in milliseconden. Deze is standaard 0 (geen wachttijd). Als N de waarde van deze wachttijd is, wordt elke actie van de gebruiker uitgevoerd na een wachttijd van N milliseconden. Zo kun je zien hoe de applicatie het wachten afhandelt;
  • in [10], de URL van de Spring 4-server. Als we het voorgaande volgen, is dit [http://localhost:8080];
  • in [11] en [12], de gebruikersnaam en het wachtwoord van degene die de applicatie wil gebruiken. Er zijn twee gebruikers: admin/admin (login/wachtwoord) met een rol (ADMIN) en user/user met een rol (USER). Alleen de rol ADMIN heeft het recht om de applicatie te gebruiken. De rol USER is er alleen om te laten zien wat de server in dit gebruiksscenario antwoordt;
  • in [13], de knop waarmee verbinding met de server kan worden gemaakt;
  • in [14], de taal van de applicatie. Er zijn er twee: standaard Frans en Engels.
  • in [1] log je in;
  • zodra je bent ingelogd, kun je de arts kiezen bij wie je een afspraak wilt maken [2] en de dag waarop die plaatsvindt [3];
  • je vraagt in [4] om de agenda van de gekozen arts voor de gekozen dag te bekijken;
  • zodra je de agenda van de arts hebt ontvangen, kun je een tijdvak reserveren [5];
  • in [6] kiest men de patiënt voor de afspraak en bevestigt men deze keuze in [7];

Zodra de afspraak is bevestigd, keert men automatisch terug naar de agenda, waar de nieuwe afspraak nu is opgenomen. Deze afspraak kan later worden verwijderd via [7].

De belangrijkste functies zijn nu beschreven. Ze zijn eenvoudig. De functies die niet zijn beschreven, zijn navigatiefuncties om terug te keren naar een vorige weergave. Laten we afsluiten met het taalbeheer:

  • in [1] schakelt u over van het Frans naar het Engels;
  • naar [2], wordt de weergave in het Engels weergegeven, inclusief de kalender;

3.8.6. De module [main.js]

De module [main.js] definieert de Angular-module die de applicatie zal aansturen:

 
  • regel 4: de module heet [rdvmedecins];
  • regel 5: de module [ngRoute] wordt gebruikt voor de routing van URL;
  • regel 6: de module [translate] wordt gebruikt voor de internationalisering van teksten;
  • regel 7: de module [base64] wordt gebruikt om de tekenreeks 'login:password' in Base64 te coderen;
  • regel 8: de module [ngLocale] wordt gebruikt voor de internationalisering van de kalender;
  • regel 9: de module [ui.bootstrap] wordt gebruikt voor de kalender;
  • regel 12: de configuratie van de routes;
  • regel 40: de internationalisering van de berichten;

3.8.7. De controller van de masterpagina

Laten we nog eens kijken naar de code HTML van de hoofdpagina [app.html]:


<body ng-controller="appCtrl">
<div class="container">
...

Regel 1: de volledige body van de masterpagina wordt aangestuurd door de controller [appCtrl]. Door zijn positie is dit de algemene en belangrijkste controller van de applicatie. Zoals uitgelegd in paragraaf 3.7.15 wordt het door deze controller opgebouwde model overgenomen door alle weergaven die in de masterpagina worden ingevoegd.

De code ervan is als volgt:


angular.module("rdvmedecins")
  .controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
    function ($scope, config, utils, $location, $locale) {

      // debug
      utils.debug("[app] init");

      // ----------------------------------------pagina-initialisatie
      // de sjablonen van de # pagina's
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // sjabloon van de huidige pagina
      var app = $scope.app;
      ...

      // ---------------------------------- methoden

      // huidige taak annuleren
      app.cancel = function () {
...
      };

      // afmelden
      app.deconnecter = function () {
        ...
      };

      // deze code moet hier blijven staan omdat deze verwijst naar de voorgaande functie [cancel]
      app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
    }])
;

De regels 10-14 definiëren de vijf sjablonen die in de applicatie worden gebruikt:

Modèle
Vue
Contrôleur
$scope.app
app.html
appCtrl
$scope.login
login.html
loginCtrl
$scope.home
home.html
homeCtrl
$scope.resa
resa.html
resaCtrl
$scope.agenda
agenda.html
agendaCtrl

Het is belangrijk om te begrijpen dat het object [$scope], dat het model van de controller van de hoofdpagina is, door alle weergaven en controllers wordt overgenomen. Daardoor heeft de controller [loginCtrl] toegang tot de elementen van [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda]. Met andere woorden: een controller heeft toegang tot de modellen van andere controllers. De onderzochte applicatie vermijdt zorgvuldig het gebruik van deze mogelijkheid. Zo werkt de controller [loginCtrl] bijvoorbeeld met slechts twee modellen:

  • zijn eigen model [$scope.login];
  • en dat van de bovenliggende controller [$scope.app];

Hetzelfde geldt voor alle andere controllers. Het model [$scope.app] zal worden gebruikt als gedeeld geheugen tussen de verschillende controllers. Wanneer een controller C1 informatie moet doorgeven aan de controller C2, gaat men als volgt te werk:

In [C1]:

$scope.app.info=value ;

In [C2]:

var value=$scope.app.info ;

In beide gevallen wordt $scope overgenomen van de controller [appCtrl] en is het dus identiek (het is een pointer) in [C1] en [C2]. Het object [$scope.app], dat dient als gedeeld geheugen tussen de controllers, wordt in de commentaren vaak session genoemd, naar het voorbeeld van de sessie die in klassieke webapplicaties wordt gebruikt en die het gedeelde geheugen tussen opeenvolgende HTTP-verzoeken aanduidt.

Laten we teruggaan naar de code van de controller [appCtrl]:


      // de sjablonen van de # pagina's
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // sjabloon van de huidige pagina
      var app = $scope.app;
      // [app.debug] en [utils.verbose] moeten altijd gesynchroniseerd blijven
      app.debug = utils.verbose;
      app.debug.on = config.debug;
      // momenteel geen paginatitel
      app.titre = {show: false};
      // geen navigatiebalken
      app.navbarrun = {show: false};
      app.navbarstart = {show: false};
      // geen fouten
      app.errors = {show: false};
      // standaardlokalisatie
      angular.copy(config.locales['fr'], $locale);
      // de huidige weergave
      app.view = {url: undefined, model: {}, done: false};
      // de huidige taak
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
  • regel 8: [$scope.app] wordt het sjabloon van de hoofdpagina. Dit is tevens het gedeelde geheugen tussen de verschillende controllers. In plaats van overal [$scope.app.champ=value] te schrijven, wordt de pointer [$scope.app] toegewezen aan de variabele [app] en schrijven we vervolgens [app.champ=value]. Je moet gewoon onthouden dat [app] het model is dat op de masterpagina wordt weergegeven;
  • regel 11: [app.debug.on] is een booleaanse waarde die de modus debug van de toepassing regelt. Standaard is deze ingesteld op true. De waarde ervan is gekoppeld aan het selectievakje [debug] in de navigatiebalken;
  • regel 15: [app.navbarrun.show] regelt de weergave van de volgende navigatiebalk:

Image

  • regel 16: [app.navbarstart.show] regelt de weergave van de volgende navigatiebalk:

Image

  • regel 18: [app.errors] is de sjabloon voor de foutbalk;

Image

  • regel 22: [app.view] bevat informatie over de huidige weergave, die momenteel wordt weergegeven door de tag [ng-view] van de masterpagina. Hierin vermelden we de volgende informatie:
    • [url]: de URL van de huidige weergave, bijvoorbeeld [/agenda];
    • [model]: het sjabloon van de huidige weergave, bijvoorbeeld [$scope.agenda];
    • [done]: geeft aan dat vrai aangeeft dat de huidige weergave haar taak heeft voltooid en dat er wordt overgeschakeld naar een andere weergave;

Deze informatie wordt gebruikt voor het beheer van de navigatie.

  • regel 24: start een asynchrone taak, een gesimuleerde wachttijd. Naar de asynchrone taak wordt verwezen door twee pointers: [app.view.model.task.action] en [app.task];

Er zijn twee methoden uitgesplitst in de controller [appCtrl]:


      // huidige taak annuleren
      app.cancel = function () {
...
      };

      // afmelden
      app.deconnecter = function () {
        ...
};
  • regel 2: de functie [app.cancel] dient om de huidige taak te annuleren waarvoor momenteel een wachtbericht wordt weergegeven. Alle weergaven tonen dit bericht en daarom vindt de annulering van de taak hier plaats;
  • regel 7: de functie [app.deconnecter] brengt de gebruiker terug naar de authenticatiepagina. Alle weergaven, behalve de weergave [/login], bieden deze mogelijkheid;

De functie [app.deconnecter] is als volgt:


      // afmelden
      app.deconnecter = function () {
        // terug naar de inlogpagina
        $location.path(config.urlLogin);
};
  • regel 4: men keert terug naar de inlogpagina van URL [/login];

3.8.8. Beheer van de asynchrone taak

In onze applicatie wordt op een bepaald moment slechts één asynchrone taak uitgevoerd. Het is mogelijk om er meerdere te hebben. Bij het opstarten van de applicatie vraagt deze bijvoorbeeld de webdienst om de lijst met artsen en vervolgens die met klanten via twee opeenvolgende verzoeken HTTP. We zouden hetzelfde kunnen doen met twee gelijktijdige verzoeken HTTP. Angular biedt de tools voor dit beheer. Hier hebben we daar niet voor gekozen.

De lopende taak wordt geannuleerd met de volgende code in de controller [appCtrl]:


      // huidige taak annuleren
      app.cancel = function () {
        utils.debug("[app] cancel task");
        // de asynchrone taak van de huidige weergave annuleren
        var task = app.view.model.task;
        task.isFinished = true;
        task.action.reject();

        ...
};
  • regel 5: de taak wordt opgezocht in [app.view.model.task]. Alle controllers zorgen er dan ook voor dat hun asynchrone taken naar dit object verwijzen;
  • regel 6: om aan te geven dat de taak is voltooid;
  • regel 7: om de taak met een fout af te sluiten. Deze notatie verschilt van die in de bestudeerde Angular-voorbeelden:
    • in de voorbeelden was het object [task] een object [$q.defer()] dat kon worden beëindigd;
    • in de definitieve versie is het object [task] een object met de velden [action, isFinished], waarbij [action] het object [$q.defer()] is datkan worden voltooid en [isFinished] een booleaanse waarde is die aangeeft dat de actie is voltooid;

Laten we de levenscyclus van het object [task] aan de hand van een voorbeeld bekijken. Bij het opstarten neemt, na de controller [appCtrl], de controller [loginCtrl] het over om de weergave [views/login.html] weer te geven. De initialisatiecode ervan is als volgt:


      // het bovenliggende model ophalen
      var login = $scope.login;
      var app = $scope.app;
      // huidige weergave
app.view = {url: config.urlLogin, model: login, done: false};

Op regel 5 staat [model=login]. Dit betekent dat wanneer het object [login] wordt gewijzigd, ook het object [app.view.model] en dus [$scope.app.view.model] worden gewijzigd. Wanneer men in de controller [loginCtrl] een gesimuleerde wachttijd wil instellen, schrijft men:


// gesimuleerde wachttijd
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};

Door het veld [task] toe te voegen aan het object [login], is het dus toegevoegd aan het object [$scope.app.view.model]. Als de gebruiker de wachtrij annuleert, wordt de code in [appCtrl.cancel]:


// model van de huidige pagina
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();

zal de gesimuleerde wachtrij wel degelijk beëindigen (regels 4-6).

3.8.9. Controle van de navigatie

De navigatieregels die in de applicatie worden gebruikt, zijn de volgende:

URL cible
URL précédente
Navigation autorisée
/login
willekeurig
ja
/home
/login
ja, als de controleur [loginCtrl] heeft aangegeven dat hij zijn werk heeft voltooid

/home
ja

/agenda
ja
/agenda
/home
ja, als de controleur [homeCtrl] heeft aangegeven dat hij zijn werk heeft voltooid

/resa
ja

/agenda
ja
/resa
/agenda
ja, als de controleur [homeCtrl] heeft aangegeven dat hij zijn werk heeft voltooid

/resa
ja

Dit wordt geïmplementeerd met de volgende code:

Voor [agendaCtrl]:

Image

  • regels 11-20: implementatie van de navigatieregel;
  • regel 26: nieuwe huidige weergave;

Voor [resaCtrl]:

Image

  • regels 12-20: implementatie van de navigatieregel:
  • regel 27: nieuwe huidige weergave;

Voor [loginCtrl]:

Image

  • hier is er geen navigatiecontrole, aangezien de regel bepaalt dat men van overal naar URL [/login] kan gaan. Dus als de gebruiker deze URL in zijn browser invoert, zal dit werken ongeacht de huidige weergave;
  • regel 16: de nieuwe huidige weergave;

De code voor de controller [homeCtrl] is gegeven in paragraaf 3.8.7.

Tot slot, voor een regel zoals:

/agenda
/home
ja, als de controller [homeCtrl] heeft aangegeven dat hij zijn werk heeft voltooid

Hier is een voorbeeld van code die de overgang regelt van URL [/home] naar URL [/agenda]:

 

Hierboven bevinden we ons in de methode [afficherAgenda] van de controller [homeCtrl]. De gebruiker heeft de agenda van een arts opgevraagd.

  • regel 107: de belofte van de taak HTTP;
  • regel 109: de variabele [app] is geïnitialiseerd met [$scope.app]. Dit laatste object wordt, zoals we hebben gezien, gebruikt als sjabloon voor de weergave [app.html]. Dit sjabloon [$scope.app] wordt ook gebruikt om de informatie op te slaan die tussen de weergaven moet worden gedeeld;
  • regel 111: de door de taak geretourneerde foutcode wordt geanalyseerd;
  • regel 113: het resultaat [result.data] wordt in het model [app] geplaatst;
  • regel 116: de controller [homeCtrl] draagt het stokje over aan de controller [agendaCtrl]. Hij geeft aan dat hij zijn werk heeft voltooid met de code uit regel 115. Deze code wordt door de controller [agendaCtrl] als volgt verwerkt:

Image

  • regel 11: het object [$scope.app.view] wordt opgehaald;
  • regel 15: verwerking van het veld [$scope.app.view.done], geïnitialiseerd door [homeCtrl];

3.8.10. De diensten

  

De diensten [config, utils, dao] zijn dezelfde als die al zijn beschreven bij de presentatie van Angular:

  • de service [config] is geïntroduceerd in paragraaf 3.7.4;
  • de service [utils] werd geïntroduceerd in paragraaf 3.7.5;
  • de service [dao] werd geïntroduceerd in paragraaf 3.7.6;

Ter herinnering volgt hieronder de structuur van deze diensten:

Dienst [config]

  • in [1]: we zien dat de code ongeveer 250 regels telt. Het belangrijkste onderdeel van deze code is het externaliseren van de sleutels van de geïnternationaliseerde berichten [2]. We vermijden het om deze sleutels hard te coderen in de code;

Service [utils]

 
  • regel 8: we waren de variabele [verbose] nog niet tegengekomen. Deze stuurt de functie [debug] als volgt aan:
 
  • regels 22-25: de functie [utils.debug] doet niets als [verbose.on] wordt geëvalueerd als false. Deze variabele is gekoppeld aan een variabele van de controller [appCtrl]:
 
  • regel 21: [app.debug] neemt de waarde aan van de pointer [utils.verbose]. Elke wijziging die wordt aangebracht in [app.debug], wordt dus ook doorgevoerd in [utils.verbose];
  • regel 22: de beginwaarde van [app.debug.on] wordt uit het configuratiebestand gehaald. Standaard is dit de waarde true.. Deze waarde kan in de loop van de tijd veranderen. De gebruiker heeft namelijk de mogelijkheid om deze te wijzigen in de navigatiebalken:
 
  • regel 45: via een selectievakje (type=checkbox) kan de waarde van [app.debug.on] (attribuut ng-model) worden gewijzigd;

Service [dao]

 

3.8.11. De richtlijnen

  

De richtlijnen [errors, footable, list, waiting] zijn dezelfde als die al zijn beschreven bij de presentatie van Angular:

  • de richtlijn [footable] werd geïntroduceerd in paragraaf 3.7.8.6;
  • de richtlijn [list] werd geïntroduceerd in paragraaf 3.7.12;
  • de richtlijnen [errors] en [waiting] zijn geïntroduceerd in paragraaf 3.7.14;

We waren de richtlijn [debug] nog niet tegengekomen. Deze luidt als volgt:

 

Het bestand [debug.html] waarnaar in regel 11 wordt verwezen, is het volgende:

 
  • regel 2: de richtlijn [debug] geeft zijn sjabloon in het formaat JSON weer in een Bootstrap-banner (regel 1);

Deze instructie wordt alleen gebruikt in de masterpagina [app.html]:

 
  • de instructie [debug] wordt gebruikt op regel 35. Deze geeft dus de vorm JSON van het sjabloon [$scope.app] weer wanneer men zich in de debugmodus bevindt (attribuut ng-show). Dit levert bijvoorbeeld het volgende op:

Om dit te kunnen interpreteren is een grondige kennis van de code vereist, maar zodra men die heeft verworven, wordt de bovenstaande informatie nuttig voor het debuggen. Hier zijn de elementen van het weergegeven model [$scope.app] gemarkeerd. Ter herinnering: [$scope.app] is het geheugen dat door de controllers wordt gedeeld;

  • [waitingBeforeTask]: de gesimuleerde wachttijd vóór elk verzoek aan HTTP;
  • [debug]: de debugmodus – is noodzakelijkerwijs true als deze balk wordt weergegeven;
  • [navbarrun]: booleaanse waarde die bepaalt of de volgende navigatiebalk wordt weergegeven:

Image

  • [navbarstart]: booleaanse waarde die bepaalt of de volgende navigatiebalk wordt weergegeven:

Image

  • [errors]: sjabloon van de richtlijn [errors];
  • [view]: bevat informatie over de momenteel weergegeven weergave;
  • [waiting]: sjabloon van de richtlijn [waiting];
  • [serverUrl, username, password]: de inloggegevens voor de webservice;
  • [medecins]: sjabloon voor de richtlijn [list] die van toepassing is op artsen;
  • [clients]: idem voor klanten;
  • [menu]: regelt de weergegeven menuopties. Deze worden gedefinieerd in [navbar-run.html]:

Image

De menuopties staan op de regels 16, 23, 29 en 36.

  • [formattedJour]: de geselecteerde dag in de kalender in het formaat 'jjjj-mm-dd';
  • [agenda]: de agenda van de arts. Hierin staan vrije tijdvakken (rv==null) en gereserveerde tijdvakken. Bij de gereserveerde tijdvakken staat de naam van de klant die de afspraak heeft gemaakt;
  • [selectedCreneau]: het tijdvak dat is gekozen om een reservering te maken;

3.8.12. De controller [loginCtrl]

  

De controller [loginCtrl] is gekoppeld aan de weergave [views/login.html], die in combinatie met de hoofdpagina de volgende pagina genereert:

Image

De controller [loginCtrl] is als volgt:

Image

  • regel 13: [login] wordt het sjabloon voor de huidige weergave;
  • regel 14: [app] is het gedeelde geheugen tussen de controllers;
  • regel 16: [app.view] wordt gevuld met de gegevens van de huidige weergave;

Deze initialisatiecode komt in elke controller voor. Voor de controller C1 van een weergave V1 met het model M1 geldt de volgende initialisatiecode:

1
2
3
var app=$scope.app;
var M1=$scope.M1;
app.view={url: config.urlV1, model:M1, done:false};
  • regel 18: misschien herinner je je nog dat [appCtrl] een gesimuleerde wachtroutine heeft gestart waarnaar wordt verwezen door het object [app.task.action]. We gebruiken de [promise] van deze taak om te wachten tot deze is voltooid;
  • regel 39: de methode [login.setLang] regelt de taalwisseling;
  • regel 47: de methode [login.authenticate] regelt de authenticatie van de gebruiker;

Laten we eens kijken naar de belangrijkste stappen van de authenticatiemethode:

Image

  • regels 50-51: [app.waiting] is de sjabloon voor de wachtbanner;
  • regel 53: [app.errors] is het sjabloon voor de foutmeldingsbanner;
  • regel 55: er wordt een gesimuleerde wachtrij gestart. Het object [action, isFinished] wordt gerefereerd door [login.task] en dus, aangezien [app.view.model=login], door [app.view.model.task]. Ter herinnering: dit is de voorwaarde om de taak te kunnen annuleren;
  • regel 57: na afloop van de gesimuleerde wachttijd worden de artsen geladen;
  • regel 62: zodra de aanvraag voor artsen is verkregen, wordt deze aanvraag geanalyseerd. Als de artsen zijn verkregen, wordt vervolgens om de klanten gevraagd;
  • regel 83: het ontvangen antwoord wordt geanalyseerd en het eindresultaat wordt weergegeven. Dit gebeurt met de volgende code:

Image

  • regel 87: de booleaanse variabele [task.isFinished] wordt in de volgende gevallen ingesteld op true:
    • de gebruiker heeft het wachten geannuleerd;
    • het verzoek van de artsen is met een fout beëindigd;
  • regels 91-98: het geval waarin we de klanten hebben gehad;
  • regel 93: [app.clients] is het sjabloon van de richtlijn [list] die de klanten in een vervolgkeuzelijst zal weergeven;
  • regels 97-98: we bereiden ons voor op het wisselen van weergave (regel 98), maar geven eerst aan dat de controller zijn werk heeft voltooid (regel 97). Ter herinnering: [$scope.app.view.done] wordt gebruikt voor de navigatiecontrole;

Het belangrijkste om hier op te merken is dat de artsen en klanten in de cache van de browser zijn opgeslagen. Ze zullen voortaan niet meer bij de webservice worden opgevraagd.

3.8.13. De controller [homeCtrl]

  

De controller [homeCtrl] is gekoppeld aan de weergave [views/home.html], die in combinatie met de masterpagina de volgende pagina genereert:

Image

De structuur van de controller [homeCtrl] is als volgt:

Image

  • regels 12-20: dit is de navigatiecontrole. Alle controllers hebben deze, behalve [loginCtrl], omdat de pagina [/login.html] onvoorwaardelijk toegankelijk is;

Image

  • regels 25-28: hier vinden we regels die vergelijkbaar zijn met die in de controller [loginCtrl]. [home] is dus het sjabloon van de weergave die aan de controller is gekoppeld;
  • regel 33: een attribuut dat we nog niet eerder zijn tegengekomen. Dit is het model van de titelbalk van de weergave:

Image

  • regel 36: [home.datepicker] is het sjabloon voor de kalender;
  • regel 38: [app.menu] is het sjabloon voor het navigatiemenu. Hier zal de optie [Agenda] aanwezig zijn. Hiermee kan de agenda van een arts worden opgevraagd;

Ten slotte beschikt de controller over twee methoden:

Image

De weergave van de agenda (regel 51) is behandeld in paragraaf 3.7.8.

3.8.14. De controller [agendaCtrl]

  

De controller [agendaCtrl] is gekoppeld aan de weergave [views/agenda.html], die in combinatie met de masterpagina de volgende pagina genereert:

Image

De structuur van de controller [agendaCtrl] is als volgt:

Image

  • de regels 10-20 zorgen voor de navigatiecontrole;

Image

  • regels 23-26: [agenda] wordt het sjabloon voor de weergave die gekoppeld is aan de controller [agendaCtrl];
  • regels 36-44: [app.titre] is het sjabloon voor de volgende titelbalk:

Image

  • regel 46: het menu zal de optie [Home / Accueil] bevatten:

Image

De methoden van de controller zijn als volgt:

Image

  • regel 95: de methode [agenda.supprimer] is behandeld in paragraaf 3.7.9;

De methode [agenda.home] is een pure navigatiemethode:

Image

De methode [agenda.reserver] is als volgt:

Image

  • regel 73: de parameter van de functie [reserver] is het nummer van het tijdslot (id);
  • regels 77-86: zijn bedoeld om het tijdslot met deze identificatiecode te vinden;
  • regel 82: het gevonden tijdslot wordt opgeslagen in het gedeelde geheugen [app]. De controller [resaCtrl], die het overneemt (regel 90), gebruikt deze informatie om zijn titelbalk weer te geven;
  • regels 89-90: navigatie naar [/resa.html];

3.8.15. De controller [resaCtrl]

  

De controller [resaCtrl] is gekoppeld aan de weergave [views/resa.html], die in combinatie met de masterpagina de volgende pagina genereert:

Image

De structuur van de controller [resaCtrl] is als volgt:

Image

  • regels 12-20: de navigatiecontrole;

Image

  • regels 24-27: [resa] wordt het sjabloon voor de huidige weergave;
  • regels 38-45: [app.titre] is de sjabloon voor de volgende titelbalk:

Image

  • regel 47: er worden twee menuopties weergegeven:

Image

De methoden van de controller zijn als volgt:

Image

De methode [resa.valider] is in paragraaf 3.7.9 besproken.

3.8.16. Taalbeheer

Alle controllers bieden de volgende methode [setLang]:

Image

Deze had kunnen worden ondergebracht in de controller [appCtrl].