AJAX, wat is het, hoe werkt het en waarom zou ik het (niet) gebruiken?

Trefwoorden: AJAX, Javascript, XML, XMLHttpRequest, form, formulier, submit

Opmerking
Dit artikel gaat er van uit dat u reeds over een stevige basiskennis van zowel HTML en XML als Javascript beschikt.


1. Inleiding


AJAX is een term die veel mensen bekend in de oren klinkt. Of het nu gaat over de Griekse Held uit de Trojaanse oorlog, de Nederlandse voetbalploeg of zelfs het gekende wasproduct, iedereen heeft er al wel eens van gehoord. In dit artikel wil ik het echter niet hebben over kraakwitte voetbaltenuetjes uit de Klassieke Oudheid… In de tekst die volgt, gaan we de technologie rond AJAX op het Internet van naderbij bekijken, samen met een kritische noot m.b.t. de bruikbaarheid in de praktijk.

2. Wat is AJAX?


AJAX is een acroniem dat staat voor Asynchronous Javascript And XML. Javascript en XML slaan terug op de gekende technologieŽn op het web en blijven in AJAX ook ongewijzigd. Asynchronous is opgenomen in de terminologie omdat de data in een bepaalde toepassing, onafhankelijk van andere, geladen kan worden met behulp van een XMLHttpRequest-object. Onafhankelijk van andere data betekent dat de gegevens op een pagina in delen geladen (of herladen!) kunnen worden.

Een voorbeeld hiervan is een chatbox waarbij de berichten opnieuw geladen worden, zonder dat de rest van de pagina opnieuw geladen dient te worden. In principe hoef je dus zelfs niet te merken dat de gegevens vernieuwd worden.

3. Uitgewerkt voorbeeld


Een andere toepassing kan bijvoorbeeld validatie van een formulier zijn. Stel dat je bezoeker zijn/haar naam en woonplaats moet invullen en je deze gegevens vervolgens wil gaan controleren. Je kan dan met behulp van XML en Javascript gaan nakijken of je formulier wel goed ingevuld is, zonder in principe je pagina zichtbaar te verversen. Maar wat met mensen die het gebruik van Javascript afzweren hoor ik de alerte lezer van dit artikel denken. Wel, net zoals altijd valt dit meestal ook te omzeilen zolang je maar doordacht je code schrijft. De validatie van de ingevoerde gegevens treedt in gang bij het submitten van het formulier. Normaal gezien wordt, na het activeren van de submitknop, het formulier verzonden. We kunnen dit (via Javascript) echter voorkomen of beter gezegd: uitstellen.

3.1. De normale werkwijze

CODE
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml">
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
  5. <title>Formulier</title>
  6. </head>
  7. <body>
  8. <div id="bericht"></div>
  9. <form action="submit.php" method="post">
  10.         <label for="naam">Naam: </label><input type="text" id="naam" name="naam" /><br />
  11.         <label for="woonplaats">Woonplaats: </label><input type="text" id="woonplaats" name="woonplaats" /><br />
  12.         <input type="submit" name="submit" />
  13. </form>
  14. </body>
  15. </html>


3.2. De code indien Javascript gebruikt kan worden

CODE
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml">
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
  5. <title>Formulier</title>
  6. </head>
  7. <body>
  8. <div id="bericht"></div>
  9. <form action="submit.php" method="post" onsubmit="return controleerGegevens()">
  10.         <label for="naam">Naam: </label><input type="text" id="naam" name="naam" /><br />
  11.         <label for="woonplaats">Woonplaats: </label><input type="text" id="woonplaats" name="woonplaats" /><br />
  12.         <input type="submit" name="submit" />
  13. </form>
  14. </body>
  15. </html>


Zoals je kan zien, is er in de de form-tag een stukje bijgekomen, meer bepaald: onsubmit="return controleerGegevens()". Dit stuk zorgt ervoor dat, wanneer we het formulier versturen, er eerst een Javascript-functie wordt aangeroepen. Tot zover geen verschil met reeds bestaande werkwijzen. In deze Javascript-functie begint het AJAX-verhaal echter. We gaan de velden niet client-side nakijken. Via AJAX gaan we de ingevoerde gegevens doorsturen naar een andere (XML-) pagina. In deze pagina plaatsen we de code die we normaal zouden uitvoeren wanneer het formulier verzonden wordt. Hoe de pagina technisch werkt, is niet zo zeer van belang; momenteel houden we enkel in ons achterhoofd dat de gegevens verwerkt worden en dat er een boodschap wordt teruggegeven.

Voor de lezers die de bovenstaande code goed hebben bekeken, konden merken dat er een div-element in is opgenomen. Dit div-element zullen we gebruiken om die boodschap waarover ik het zonet had in te weergeven. Zoals altijd, dien je er van uit te gaan dat je bezoekers dom zijn. Er kunnen meerdere dingen fout gaan; gegevens kunnen bijvoorbeeld (al dan niet bewust) foutief ingevuld worden. In eerste instantie gaan we er van uit dat er inderdaad een fout is opgetreden; laat ons zeggen dat de woonplaats niet werd ingevuld. Normaal gezien werd dit pas opgemerkt als de volgende pagina geladen was. Dankzij AJAX kunnen we dat foutbericht nu reeds tonen, in die div. Natuurlijk kan bij het controleren van de gegevens reeds een verbinding met (bijvoorbeeld) de gegevensbank gemaakt worden om ze weg te schrijven. In dit geval kan je via return false de verdere verzending van het formulier tegengaan. Je kan de toepassing van AJAX nog verderzetten: zet de form-tag in een DIV en verander (via Javascript) gewoon de inhoud zodat het niet meer getoond wordt ;).

Overigens, voor de mensen die een browser gebruiken waarbij Javascript-code niet wordt uitgevoerd, zal het formulier (bij het activeren van de submit-knop) gewoon verzonden worden, zoals je dat zou verwachten.

4. Genoeg randinformatie, nu code!


4.1. Het XMLHttpRequest-object

AJAX werkt met behulp van (minimum) ťťn XMLHttpRequest-object. Hoe je dit object kan aanmaken, verschilt van browser tot browser; concreet is er enkel een verschil tussen Microsoft Internet Explorer en andere browsers als Opera, Safari, Mozilla, etc. In het geval van Microsoft Internet Explorer dienen we (spijtig genoeg) gebruik maken van ActiveX, waarbij we er rekening mee moeten houden dat dit uitgeschakeld kan worden. Een eenvoudige manier om een XMLHttpRequest-object aan te maken in elke browser (mits deze AJAX ondersteunt natuurlijk) kan je hieronder vinden.

CODE
  1. function getXMLHttpRequester()
  2. {
  3.        
  4.         var xmlHttpRequest = false;
  5.                        
  6.         // Proberen een nieuw object aan te maken.
  7.         try
  8.         {
  9.        
  10.                 // Nakijken of de browser Microsoft Internet Explorer is
  11.                 if(window.ActiveXObject)
  12.                 {
  13.                        
  14.                         // Omdat elke versie van Internet Explorer een andere versie kan nodig hebben,
  15.                         // overlopen we elke mogelijke versie; van hoog naar laag.
  16.                         for(var i = 5; i; i--)
  17.                         {
  18.                                
  19.                                 try
  20.                                 {
  21.                                        
  22.                                         // De laatste versie kan niet geladen worden, dus moeten we teruggrijpen
  23.                                         // naar een oudere variant.
  24.                                         if(i == 2)
  25.                                         {
  26.                                                
  27.                                                 xmlHttpRequest = new ActiveXObject("Microsoft.XMLHTTP");       
  28.                                                
  29.                                         // De laatste mogelijke versie laden
  30.                                         } else {
  31.                                                
  32.                                                 xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP." + i + ".0");
  33.                                                
  34.                                         }
  35.                                        
  36.                                         break;
  37.                                        
  38.                                 }
  39.                                
  40.                                 // Indien het object niet gemaakt kan worden, moeten we dit helaas zo teruggeven.
  41.                                 catch(excNotLoadable)
  42.                                 {
  43.                                                        
  44.                                         xmlHttpRequest = false;
  45.                                        
  46.                                 }
  47.                                
  48.                         }
  49.                        
  50.                 }
  51.                
  52.                 // Browsers als Opera, Mozilla (Firefox) en Safari kunnen de 'gewone' manier gebruiken
  53.                 else if(window.XMLHttpRequest)
  54.                 {
  55.                        
  56.                         xmlHttpRequest = new XMLHttpRequest();
  57.                        
  58.                 }
  59.                
  60.         }
  61.        
  62.         // Het is mogelijk dat de browser het object aanmaken niet ondersteunt; ook in dit geval sturen we
  63.         // dan ook geen object terug.
  64.         catch(excNotLoadable)
  65.         {
  66.                
  67.                 xmlHttpRequest = false;
  68.                
  69.         }
  70.        
  71.         // Het gemaakte object (of 'false' in het geval dat er een probleem was) teruggeven
  72.         return xmlHttpRequest;
  73.        
  74. }


Om nu een effectief een object aan te maken, kan je gewoon gebruik maken van deze korte code:

CODE
  1. var xmlHttpRequest = getXMLHttpRequester();


4.2. Requests maken via GET

Requests maken aan de hand van een eerder aangemaakt object is niet zo moeilijk. De eerste stap is dus een object aanmaken. Daaronder plaatsen we, zoals je kan zien, nog een extra regel. De reden hiervoor is dat sommige browser de request foutief afhandelen indien deze niet het XML-MIME-type via de headers krijgt doorgegeven. Soms werkt het echter wel, maar we kunnen beter op zeker spelen en het MIME-type gewoon zelf bepalen.

CODE
  1. var xmlHttpRequest = getXMLHttpRequester();
  2. xmlHttpRequest.overrideMimeType('text/xml');


Nadat we een nieuw object hebben aangemaakt, moeten we nog aangeven wat er moet gebeuren wanneer er een respons ontvangen wordt. Concreet kan dit gewoon een functie zijn:

CODE
  1. xmlHttpRequest.onreadystatechange = function ()
  2. {
  3.        
  4.         // Hier plaats je de code die moet aangeroepen worden indien het script geladen is
  5.         if (xmlHttpRequest.readyState == 4)
  6.         {
  7.                
  8.                 // Indien de pagina ingeladen kon worden, tonen we de inhoud van
  9.                 // die pagina in een dialoogvenster.
  10.                 alert(xmlHttpRequest.responseText);
  11.                
  12.         } else {
  13.                
  14.                 // Pagina is nog niet volledig ingeladen.
  15.                 // In principe kan je de else-sectie gewoon weglaten, tenzij je specifiek
  16.                 // ook een actie wil aanroepen indien de pagina nog niet geladen is.
  17.                
  18.         }
  19.  
  20. }


In de bovenstaande functie kijken we eerst of de pagina volledig ingeladen is, dit kunnen we nagaan door te onderzoeken welke waarde readyState bevat:

  • 0: verbinding nog niet gemaakt
  • 1: bezig met inladen
  • 2: ingeladen
  • 3: interactief
  • 4: volledig ingeladen

Een methode die op het eerste zicht perfect lijkt te werken, maar wat indien een pagina nu niet gevonden kan worden? In dat geval wordt er een 404-foutmelding gegeven, die ook perfect een pagina kan zijn; om nog maar te zwijgen voor andere foutmeldingen. Om dit te voorkomen, kunnen we ook gaan controleren op basis van status. De status kunnen we afleiden uit de statuscodes. Deze codes zijn de codes die men al jaren op het Internet gebruikt om de status van een pagina aan te geven. De bekendste is ongetwijfeld 404 (pagina kon niet gevonden worden); daarnaast heb je ook 500 (interne serverfout) en de meest gebruikte: 200 (pagina correct ingeladen). Omdat een voorbeeld veel meer zegt dan een alinea tekst:

CODE
  1. xmlHttpRequest.onreadystatechange = function ()
  2. {
  3.        
  4.         // Hier plaats je de code die moet aangeroepen worden indien het script geladen is
  5.         if (xmlHttpRequest.status == 200)
  6.         {
  7.                
  8.                 // Indien de pagina ingeladen kon worden, tonen we de inhoud van
  9.                 // die pagina in een dialoogvenster.
  10.                 alert(xmlHttpRequest.responseText);
  11.                
  12.         } else {
  13.                
  14.                 // Pagina kon niet volledig ingeladen worden omdat er een probleem is opgetreden.
  15.                 // In principe kan je de else-sectie gewoon weglaten, tenzij je specifiek
  16.                 // ook een actie wil aanroepen indien de pagina niet geladen werd.
  17.                 // Om te weten wat er net fout liep, kunnen we de foutboodschap opvragen.
  18.                 alert("Er ging wat fout: \n" + xmlHttpRequest.statusText);
  19.                
  20.         }
  21.  
  22. }


Het probleem hierbij is echter dat de pagina nog aan het laden kan zijn en we dus niet de volledige gegevens doorkrijgen. Om dit te voorkomen, kunnen we bovenstaande voorbeelden combineren:

CODE
  1. xmlHttpRequest.onreadystatechange = function ()
  2. {
  3.        
  4.         // Allereerst gaan we nakijken of de pagina volledig geladen is
  5.         if (xmlHttpRequest.readyState == 4)
  6.         {
  7.                
  8.                 if (xmlHttpRequest.status == 200)
  9.                 {
  10.                        
  11.                         // Indien de pagina ingeladen kon worden, tonen we de inhoud van
  12.                         // die pagina in een dialoogvenster.
  13.                         alert(xmlHttpRequest.responseText);
  14.                        
  15.                 } else {
  16.                        
  17.                         // Pagina kon niet volledig ingeladen worden omdat er een probleem is opgetreden.
  18.                         // In principe kan je de else-sectie gewoon weglaten, tenzij je specifiek
  19.                         // ook een actie wil aanroepen indien de pagina niet geladen werd.
  20.                         // Om te weten wat er net fout liep, kunnen we de foutboodschap opvragen.
  21.                         alert("Er ging wat fout: \n" + xmlHttpRequest.statusText);
  22.                        
  23.                 }
  24.        
  25.         }
  26.  
  27. }


Nu we bepaald hebben wat er moet gebeuren nadat de request gemaakt is, moeten we natuurlijk de request zelf eerst nog maken.

CODE
  1. xmlHttpRequest.open('GET', 'pagina.php', true);
  2. xmlHttpRequest.send(null);


Niet zo heel erg veel code, maar toch van kapitaal belang voor het correct functioneren van onze pagina. Om te beginnen moeten we aangeven via welke request methode we een bepaalde pagina willen openen. De derde parameter dient om aan te geven dat de request Asynchronous (asynchroon) verloopt, vandaar ook de eerste A in AJAX. Vergeet zeker en vast niet om de eerste parameter in hoofdletters te schrijven. Indien je dit niet doet, kunnen sommige browsers last hebben met de verwerking van je request. Je kan die eerste parameter naast de waarde GET bijvoorbeeld ook de waarde POST of HEAD meegeven, naargelang de werking van je script. Meer informatie over de mogelijke waarden van de eerste parameter kan je terugvinden op de website van het WWW-consortium.

De tweede regel van de code bepaalt welke tekst we willen verzenden naar de server. Indien we GET gebruiken, mag je hier gewoon null invullen.

4.3. Requests maken via POST

Request maken via POST werkt op exact dezelfde manier als via GET. Het enigste wat je moet veranderen, zijn de twee laatste regels van het laatste codevoorbeeld uit het vorige deeltje:

CODE
  1. xmlHttpRequest.open('POST', 'pagina.php', true);
  2. http_request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  3. xmlHttpRequest.send(veld1=waarde1&veld2=waarde2&veld3=waarde3);


Zoals je kan zien, hebben we 2 zaken aangepast en daarnaast ook 1 regel toegevoegd:

  • GET wordt POST. Vanzelfsprekend dienen we aan te geven welke request method we willen gebruiken. Aangezien dit niet langer GET is, veranderen we deze parameter.
  • Indien we data naar de server willen verzenden via POST moeten we het MIME-type veranderen om te zorgen dat de server de data effectief aankrijgt en verwerkt. Indien we dit niet vermelden, zal de server de data gewoon negeren.
  • De laatste wijziging die we nog moeten aanbrengen, is de server aangeven welke data we net willen doorsturen. Dit kan je doen via de methode send. Daar waar we eerder gewoon null invulden, plaatsen we er nu effectief velden en gegevens in, gescheiden daar een ampersand.


4.4. Request maken via HEAD

Nadat we eerder reeds GET en POST van naderbij bekeken, gaan we nu een blik werpen op een derde belangrijke request method zijnde HEAD. HEAD is enorm handig indien we bijvoorbeeld gegevens over een bepaalde pagina willen opvragen, zonder dat we interesse hebben in de inhoud van die pagina. Dit kan handig zijn als we willen nagaan wanneer een pagina voor het laatste gewijzigd werd of om te zien welke software de server gebruikt, etc.

CODE
  1. function getXMLHttpRequester()
  2. {
  3.        
  4.         var xmlHttpRequest = false;
  5.                        
  6.         // Proberen een nieuw object aan te maken.
  7.         try
  8.         {
  9.        
  10.                 // Nakijken of de browser Microsoft Internet Explorer is
  11.                 if(window.ActiveXObject)
  12.                 {
  13.                        
  14.                         // Omdat elke versie van Internet Explorer een andere versie kan nodig hebben,
  15.                         // overlopen we elke mogelijke versie; van hoog naar laag.
  16.                         for(var i = 5; i; i--)
  17.                         {
  18.                                
  19.                                 try
  20.                                 {
  21.                                        
  22.                                         // De laatste versie kan niet geladen worden, dus moeten we teruggrijpen
  23.                                         // naar een oudere variant.
  24.                                         if(i == 2)
  25.                                         {
  26.                                                
  27.                                                 xmlHttpRequest = new ActiveXObject("Microsoft.XMLHTTP");       
  28.                                                
  29.                                         // De laatste mogelijke versie laden
  30.                                         } else {
  31.                                                
  32.                                                 xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP." + i + ".0");
  33.                                                
  34.                                         }
  35.                                        
  36.                                         break;
  37.                                        
  38.                                 }
  39.                                
  40.                                 // Indien het object niet gemaakt kan worden, moeten we dit helaas zo teruggeven.
  41.                                 catch(excNotLoadable)
  42.                                 {
  43.                                                        
  44.                                         xmlHttpRequest = false;
  45.                                        
  46.                                 }
  47.                                
  48.                         }
  49.                        
  50.                 }
  51.                
  52.                 // Browsers als Opera, Mozilla (Firefox) en Safari kunnen de 'gewone' manier gebruiken
  53.                 else if(window.XMLHttpRequest)
  54.                 {
  55.                        
  56.                         xmlHttpRequest = new XMLHttpRequest();
  57.                        
  58.                 }
  59.                
  60.         }
  61.        
  62.         // Het is mogelijk dat de browser het object aanmaken niet ondersteunt; ook in dit geval sturen we
  63.         // dan ook geen object terug.
  64.         catch(excNotLoadable)
  65.         {
  66.                
  67.                 xmlHttpRequest = false;
  68.                
  69.         }
  70.        
  71.         // Het gemaakte object (of 'false' in het geval dat er een probleem was) teruggeven
  72.         return xmlHttpRequest;
  73.        
  74. }
  75.  
  76. // Het object aanmaken
  77. var xmlHttpRequest = getXMLHttpRequester();
  78.  
  79. // Verbinding naar 'pagina.php' aanmaken
  80. xmlHttpRequest.open('HEAD', 'pagina.php',true);
  81. xmlHttpRequest.onreadystatechange=function()
  82. {
  83.        
  84.         if (xmlHttpRequest.readyState==4)
  85.         {
  86.                
  87.                 // Indien de pagina volledig ingeladen is, vragen we de headers op.
  88.                 var paginaHeaders = xmlHttpRequest.getAllResponseHeaders();
  89.                
  90.                 // Bij wijze van voorbeeld tonen we deze headers in een dialoogvenster
  91.                 alert("Headers van 'pagina.php': \n" + paginaHeaders);
  92.                
  93.         }
  94.        
  95.         xmlHttpRequest.send(null)
  96.        
  97. }


Stel dat we enkel geÔnteresseerd zijn in bijvoorbeeld het MIME-type van de pagina, kunnen we dit opvragen via volgende code:

CODE
  1. xmlHttpRequest.getResponseHeader('Content-Type');


4.5. Eigenschappenoverzicht van het xmlHttpRequest-object

EigenschapUitleg
onreadystatechangeAan deze eigenschap kan je bijvoorbeeld een functie koppelen om zo te onderzoeken of de pagina ingeladen kon worden.
readyStateVia deze eigenschap kan je de status van een pagina opvragen; deze status kan 5 waarden hebben:
  • 0: verbinding nog niet gemaakt
  • 1: bezig met inladen
  • 2: ingeladen
  • 3: interactief
  • 4: volledig ingeladen
responseTextDe inhoud van de opgevraagde pagina als string.
responseXMLOok via deze eigenschap kan je de inhoud van de opgevraagde bekijken, maar dan als een XML-document.
statusVia deze eigenschap kan je de status van de opgevraagde pagina bekijken. De status zal teruggegeven worden als een integer. Bijvoorbeeld:
  • 200: OK
  • 404: Not Found
  • 500: Internal Server Error
  • etc.
statusTextVia deze eigenschap kan je wederom de status van een pagina opvragen, maar dit keer zal er geen integer, maar een string worden teruggegeven.

4.6. Methodenoverzicht van het xmlHttpRequest-object

MethodeUitleg
void abort()Indien je tijdens het verwerken van gegevens de eerder gemaakte request wil beŽindigen.
Char getAllResponseHeaders()Geeft de namen en de waarden terug van alle headers van de huidige request.
getResponseHeader(String header)Geeft de waarde van bepaalde header terug.
void open(String methode, String URL, boolean Asynchrone transfer)Gebruik deze methode om een verbinding te maken via je XMLHttpObject
overrideMimeTypeDeze methode kan je gebruiken om het MIME-type dat de server opgeeft te overschrijven met een van je keuze.
void send(String gegevens)Methode om effectief de request uit te voeren. Deze methode moet je uitvoeren alvorens je een request kan uitvoeren. Indien je POST gebruikt, moet je de door te geven data via de parameter versturen.
void setRequestHeader(String naam, String waarde)Deze methode kan je gebruiken om een naam en bijhorende waarde te laten opnemen in de headers die verzonden worden.


5. Tot slot


Ondanks dat velen AJAX als een van de technologieŽn voor de toekomst zien, is ze (nog steeds) geen officiŽle standaard. Het staat buiten kijf dat AJAX een enorm potentieel heeft, maar daarnaast ook dat het door een heel aantal mensen enorm overroepen is (geweest). Zoals bij elke technologie dien je te beseffen dat deze niet door elke browser ondersteund wordt. Zowel Microsoft Internet Explorer (vanaf versie 5.0), Opera (vanaf versie 8.0), Mozilla (vanaf versie 1.0), Netscape (vanaf versie 7.0) als Safari (vanaf versie 1.2) kunnen AJAX-gebaseerde toepassingen verwerken. Grote spelers als Google en Amazon bewijzen dat AJAX wel degelijk te implementeren is, maar ook zij hebben (zeker in de beginfase) problemen gekend met de bruikbaarheid van de scripts. GMAIL is echter het levende bewijs dat AJAX ook te gebruiken is in zeer complexe toepassingen. Voor je echter met AJAX begint te werken, werp je best eens een blik op het onderstaande lijstje met links.

Dit artikel werd geschreven door Martijn op zaterdag 14 januari 2006 om 19:01 en werd sindsdien 16193 keer gelezen.

  • Pagina
  • 1 van 1

Geen reacties gevonden
Er werden nog geen reacties bij dit artikel geplaatst.
  • Indien je denkt iets te kunnen toevoegen aan het artikel, kan je zelf een reactie schrijven via de koppeling Plaats een reactie bij dit artikel hieronder.
  • Indien je andere commentaar (iets wat niet meteen functioneel bijdraagt aan het artikel zelf) hebt, kan je een bedankje formuleren via de koppeling Plaats een bedankje bij dit artikel hieronder.