In deze les zullen we een CodeIgniter-bibliotheek maken waarmee we gegevensrasters automatisch kunnen genereren voor het beheren van elke databasetabel. Ik zal elke stap uitleggen die nodig is om deze klasse te maken; dus je zult waarschijnlijk een aantal nieuwe OOP-technieken / concepten leren in het proces!
Als een bonus zullen we doorgaan met het schrijven van een aantal jQuery-code waarmee een gebruiker de inhoud van het gegevensrooster kan bijwerken zonder te hoeven wachten op een paginavernieuwing.
Deze tutorial veronderstelt dat je een bescheiden kennis hebt van de CodeIgniter- en jQuery-frameworks.
Een datagrid is een tabel die de inhoud van een database of tabel samen met sorteerbesturingselementen weergeeft.
Een datagrid is een tabel die de inhoud van een database of tabel samen met sorteerbesturingselementen weergeeft. In deze zelfstudie hebben we de taak om deze functionaliteit te bieden, maar ook om te voorkomen dat de gebruiker moet wachten tot de pagina wordt vernieuwd telkens wanneer een bewerking wordt uitgevoerd. Dankzij jQuery wordt dit een vrij eenvoudige taak!
Hoe zit het met de gebruikers die Javascript niet hebben ingeschakeld? Maak je geen zorgen, we zullen ze ook compenseren!
We willen een tool bouwen die ons in staat stelt datagrids dynamisch te maken voor elke databasetabel die we hebben. Dit betekent dat de code niet is gekoppeld aan een specifieke tabelstructuur en dus onafhankelijk is van de gegevens zelf. Alle coder (de ontwikkelaar die onze klasse gebruikt) moet weten is de naam van de tabel die moet worden omgezet in een raster en de primaire sleutel voor die tabel. Dit is het voorwoord van de les die we het grootste deel van deze tutorial zullen gaan ontwikkelen:
De Datagrid-klasse kan goed worden toegevoegd aan de map met toepassingen / bibliotheken, maar we gaan deze toevoegen als helper aan het CodeIgniter-framework. Waarom? Omdat het laden van bibliotheken ons niet toestaat argumenten door te geven aan de constructor van de klasse, waardoor het laden als hulp het probleem zal oplossen. Dit punt zal logischer voor je zijn als we klaar zijn met het schrijven van de constructor.
publieke functie __construct ($ tbl_name, $ pk_col = 'id') $ this-> CI = & get_instance (); $ This-> CI-> load-> databank (); $ this-> tbl_fields = $ this-> CI-> db-> list_fields ($ tbl_name); if (! in_array ($ pk_col, $ this-> tbl_fields)) gooi nieuwe uitzondering ("Primaire sleutel kolom '$ pk_col' niet gevonden in tabel '$ tbl_name'"); $ this-> tbl_name = $ tbl_name; $ this-> pk_col = $ pk_col; $ This-> CI-> load-> library ( 'table');
We hebben al veel aan de hand; maar maak je geen zorgen, want ik zal alles voor je uitleggen in de volgende paragraaf.
De constructor neemt twee argumenten: de eerste is de naam van de tabel in uw database die u als een datagrid voor de gebruiker wilt weergeven; de tweede param is de naam van de kolom die dient als de primaire sleutel voor die tabel (daarover later meer). In het lichaam van de constructor plaatsen we het CodeIgniter-object, het databaseobject en de HTML-tabelklasse / -bibliotheek. Deze zijn allemaal nodig gedurende de levensduur van een Datagrid-object en zijn al ingebouwd in het CI-framework. Merk op dat we ook controleren of de primaire sleutel echt bestaat in de gegeven tabel, en in het geval dat dit niet het geval is, gooien we een uitzondering die de fout meldt. Nu de $ This-> tbl_fields
lidvariabele is beschikbaar voor later gebruik, dus we hoeven de database niet opnieuw op te halen.
"We kunnen het commando gebruiken,
$ CI-> db-> list_fields ($ tbl_name)
om de namen van alle velden op te halen die een tabel heeft. Voor betere prestaties raad ik aan de resultaten in de cache op te slaan. "
public function setHeadings (array $ heading) $ this-> headings = array_merge ($ this-> headings, $ heading);
Hiermee kunt u de koppen van uw gegevensraster-tabel aanpassen - dat wil zeggen dat u de oorspronkelijke kolomnamen voor bepaalde tabelvelden kunt overschrijven. Er is een associatief voor nodig rangschikking
, zoals dit: regdate => "Registratiedatum". In plaats van alleen de technische "Regdate" als de kolomkop voor dat soort gegevens, hebben we daarvoor een meer voor de mens leesbare titel. De code die verantwoordelijk is voor het toepassen van de koppen zal binnenkort bekend worden gemaakt.
openbare functie ignoreFields (array $ -velden) foreach ($ velden als $ f) if ($ f! = $ this-> pk_col) $ this-> hide_cols [] = $ f;
ignoreFields
ontvangt een rangschikking
met de velden die moeten worden genegeerd bij het ophalen van gegevens uit de database. Dit is handig als we tabellen met veel velden hebben, maar we willen er slechts een paar verbergen. Deze methode is slim genoeg om een poging te volgen om het primaire sleutelveld te negeren en dat vervolgens over te slaan. Dit is zo omdat de primaire sleutel kan niet genegeerd worden om technische redenen (u zult snel zien waarom). Als u echter wilt voorkomen dat de kolom met de primaire sleutel wordt weergegeven in de gebruikersinterface, kunt u de hidePkCol
methode:
openbare functie hidePkCol ($ bool) $ this-> hide_pk_col = (bool) $ bool;
Deze methode ontvangt een Booleaanse waarde om aan te geven of we de kolom met primaire sleutels willen verbergen zodat deze niet in het gegevensraster wordt weergegeven. Soms is het een lelijk idee om de ptoets
gegevens, meestal een numerieke code zonder betekenis voor de gebruiker.
Volgende instantie methode:
persoonlijke functie _selectFields () foreach ($ this-> tbl_fields als $ field) if (! in_array ($ field, $ this-> hide_cols)) $ this-> CI-> db-> select ($ field); // verberg pk kolomkop? if ($ field == $ this-> pk_col && $ this-> hide_pk_col) ga verder; $ heading [] = isset ($ this-> headings [$ field])? $ this-> titels [$ field]: ucfirst ($ field); if (! empty ($ heading)) // plaats een checkbox voor het wisselen van array_unshift ($ heading, ""); $ this-> CI-> table-> set_heading ($ heading);
Hier hebben we een hulpmethode; daarom heeft het de "private" modifier en is het voorafgegaan door een onderstreept teken (codeconventie). Het zal worden gebruikt door de genereren ()
methode - binnenkort uitgelegd - om de juiste tabelvelden te selecteren en ook de juiste kopjes in de tabel (generator) voorwerp
. Let op de volgende regel:
$ heading [] = isset ($ this-> headings [$ field])? $ this-> titels [$ field]: ucfirst ($ field);
Dit is waar we de aangepaste headers of resort toepassen op de standaard headers als er geen wordt gegeven. Als het pk
kolom hoort verborgen te zijn voor weergave, dan wordt de kop overgeslagen. Merk ook de volgende regel op:
array_unshift (posten $,"");
Het bovenstaande commando geeft het programma de opdracht om een "Master" -veld in te vullen als eerste kop van de tabel. Dat selectievakje verschilt van andere selectievakjes in het raster doordat het een gebruiker toestaat om alle selectievakjes in één keer aan of uit te vinken. Deze wisselfunctionaliteit wordt in enkele ogenblikken geïmplementeerd met een eenvoudig jQuery-codefragment.
Nu komt het ding dat het echte werk voor ons doet:
public function generate () $ this -> _ selectFields (); $ rows = $ this-> CI-> db -> from ($ this-> tbl_name) -> get () -> result_array (); foreach ($ rijen als & $ rij) $ id = $ rij [$ this-> pk_col]; // vink een selectievakje aan om selectie van items / rijen array_unshift ($ row, ""); // hide pk column cell? if ($ this-> hide_pk_col) unset ($ row [$ this-> pk_col]); return $ this-> CI-> table-> genereer ($ rijen) ;
De voortbrengen
methode, zoals de naam doet vermoeden, is verantwoordelijk voor het genereren van het gegevensraster zelf. U moet deze methode alleen gebruiken nadat u het object hebt geconfigureerd op basis van uw behoeften. Het eerste dat het doet is het deze $ -> _ selectFields ()
methode om de acties uit te voeren die we eerder hebben uitgelegd. Nu moet hij alle rijen uit de database ophalen en deze vervolgens doorlopen, waarbij vakjes aan het begin van elke rij worden toegevoegd:
// vink een selectievakje aan om selectie van items / rijen array_unshift ($ row, "");
Binnen in de foreach
loop op de voortbrengen
methode, als de $ This-> hide_pk_col
vlag is ingesteld op waar
, dan moeten we de primaire sleutelinvoer in de $ rij-array
dus het zal niet verschijnen als een kolom als het $ This-> CI-> table
voorwerp
verwerkt alle rijen en genereert de uiteindelijke HTML-uitvoer. Op dit moment is het oké om de primaire sleutel te verwijderen, indien nodig, omdat we die informatie niet langer nodig hebben. EEN
Maar wat doet de gebruiker met de geselecteerde / gecontroleerde rijen? Om dit te beantwoorden, heb ik nog een paar andere methoden voorbereid. De eerste stelt ons in staat om "actieknoppen" te creëren zonder dat we technische details hoeven te weten over hoe het rastersysteem intern werkt:
openbare statische functie createButton ($ action_name, $ label) retourneer "";
Geef de naam van de actie simpelweg door als het eerste argument en een tweede argument om het label voor de gegenereerde knop aan te duiden. EEN klasse
kenmerk wordt automatisch gegenereerd voor die knop, zodat we er gemakkelijker mee kunnen spelen als we ermee werken in onze JavaScript. Maar hoe weten we of een gebruiker een bepaalde actieknop heeft ingedrukt? Het antwoord is te vinden in de volgende methode:
openbare statische functie getPostAction () // ontvang naam van ingediende actie (indien aanwezig) if (isset ($ _ POST ['dg_action'])) return-sleutel ($ _ POST ['dg_action']);
Yep! Een andere statische methode die ons helpt wanneer we met vormen te maken hebben. Als er een gegevensraster is ingediend, retourneert deze methode de naam van de actie (of "bewerking") die aan die gebeurtenis is gekoppeld. Daarnaast is nog een handige tool voor het verwerken van onze datagrid-formulieren?
openbare statische functie getPostItems () if (! empty ($ _ POST ['dg_item'])) return $ _POST ['dg_item']; return array ();
? wat een resultaat oplevert rangschikking
met de geselecteerde ids
zodat u kunt bijhouden welke rijen in het raster zijn geselecteerd en vervolgens wat actie met ze uitvoeren. Als een voorbeeld van wat kan worden gedaan met een selectie van rij ID kaart
s, ik heb een andere methode voorbereid - deze is een instantiemethode, en geen statische methode, omdat deze gebruikmaakt van de instantiemiddelen van het object om zijn activiteiten te doen:
public function deletePostSelection () // verwijder geselecteerde items uit de db als (! empty ($ _ POST ['dg_item'])) $ this-> CI-> db -> teruggeeft van ($ this-> tbl_name) -> where_in ($ this-> pk_col, $ _ POST ['dg_item']) -> delete ();
Als minstens één selectievakje is aangevinkt, is de deletePostSelection ()
methode genereert en voert een SQL-statement uit, zoals het volgende (veronderstel $ Tbl_name = 'my_table'
en $ Pk_col = 'id'
):
DELETE FROM my_table WHERE id IN (1,5,7,3, etc?)
? waarmee de geselecteerde rijen effectief uit de persistente laag worden verwijderd. Er kunnen meer bewerkingen zijn die u aan een gegevensraster kunt toevoegen, maar dat is afhankelijk van de details van uw project. Als een tip, zou je deze klasse kunnen uitbreiden naar bijvoorbeeld, InboxDatagrid
, dus, voorbij de deletePostSelection
methode, zou het extra bewerkingen kunnen omvatten, zoals moveSelectedMessagesTo ($ plaats)
, enz?
Nu, als je deze tutorial stap voor stap hebt gevolgd, zou je met iets vergelijkbaars te maken hebben gehad als het volgende:
class Datagrid private $ hide_pk_col = true; private $ hide_cols = array (); privé $ tbl_name = "; private $ pk_col ="; private $ heading = array (); privé $ tbl_fields = array (); function __construct ($ tbl_name, $ pk_col = 'id') $ this-> CI = & get_instance (); $ This-> CI-> load-> databank (); $ this-> tbl_fields = $ this-> CI-> db-> list_fields ($ tbl_name); if (! in_array ($ pk_col, $ this-> tbl_fields)) gooi nieuwe uitzondering ("Primaire sleutel kolom '$ pk_col' niet gevonden in tabel '$ tbl_name'"); $ this-> tbl_name = $ tbl_name; $ this-> pk_col = $ pk_col; $ This-> CI-> load-> library ( 'table'); public function setHeadings (array $ heading) $ this-> headings = array_merge ($ this-> headings, $ heading); openbare functie hidePkCol ($ bool) $ this-> hide_pk_col = (bool) $ bool; openbare functie ignoreFields (array $ velden) foreach ($ velden als $ f) if ($ f! = $ this-> pk_col) $ this-> hide_cols [] = $ f; persoonlijke functie _selectFields () foreach ($ this-> tbl_fields als $ field) if (! in_array ($ field, $ this-> hide_cols)) $ this-> CI-> db-> select ($ field ); // verberg pk kolomkop? if ($ field == $ this-> pk_col && $ this-> hide_pk_col) ga verder; $ heading [] = isset ($ this-> headings [$ field])? $ this-> titels [$ field]: ucfirst ($ field); if (! empty ($ heading)) // plaats een checkbox voor het wisselen van array_unshift ($ heading, ""); $ this-> CI-> table-> set_heading ($ heading); public function generate () $ this -> _ selectFields (); $ rows = $ this-> CI-> db -> from ( $ this-> tbl_name) -> get () -> result_array (); foreach ($ rijen als & $ row) $ id = $ row [$ this-> pk_col]; // vink een selectievakje aan om selectie van items mogelijk te maken array_unshift ($ rij, ""); // verberg pk kolom? if ($ this-> hide_pk_col) unset ($ row [$ this-> pk_col]); return $ this-> CI-> table-> genereer ($ rijen); public static function createButton ($ action_name, $ label) ga terug ""; openbare statische functie getPostAction () // ontvang naam van ingediende actie (indien aanwezig) if (isset ($ _ POST ['dg_action'])) return key ($ _ POST ['dg_action']); public statische functie getPostItems () if (! empty ($ _ POST ['dg_item']) retour $ _POST ['dg_item']; return array (); public function deletePostSelection () // verwijder geselecteerde items uit de db als (! empty ($ _ POST ['dg_item'])) $ this-> CI-> db -> teruggeeft van ($ this-> tbl_name) -> where_in ($ this-> pk_col, $ _ POST ['dg_item' ]) -> verwijderen ();
Opmerking: vergeet niet dit bestand op te slaan als datagrid_helper.php
, en plaats het in "application / helper /"
We zullen nu een eenvoudige testcontroller maken en de Datagrid-klasse als hulp in de constructor laden. Maar daarvoor moeten we een dummy databasetabel definiëren en deze vullen met een aantal voorbeeldgegevens.
Voer de volgende SQL uit om de database en de gebruikerstabel te maken:
CREATE DATABASE 'dg_test'; MAAK TAFEL 'users' ('id' int (11) NOT NULL AUTO_INCREMENT, 'gebruikersnaam' varchar (80) NOT NULL, 'password' varchar (32) NOT NULL, 'email' varchar (255) NOT NULL, UNIEKE SLEUTEL ' id '(' id ')) ENGINE = MyISAM DEFAULT CHARSET = latin1 AUTO_INCREMENT = 5;
Laten we er vervolgens enkele gebruikers aan toevoegen:
INSERT INTO 'users' ('id', 'username', 'password', 'email') VALUES (1, 'david', '12345', '[email protected]'), (2, 'maria', '464y3y', '[email protected]'), (3, 'alejandro', 'a42352fawet', '[email protected]'), (4, 'emma', 'f22a3455b2','[email protected] ');
Bewaar nu de volgende code als "test.php
,en voeg het toe aan de map "application / controllers":
load> helper (array ( 'grid', 'url')); $ this-> Datagrid = new Datagrid ('users', 'id'); function index () $ this-> load-> helper ('form'); $ This-> load-> library ( 'session'); $ This-> Datagrid-> hidePkCol (true); $ This-> Datagrid-> setHeadings (array ( 'email' => 'E-mail')); $ This-> Datagrid-> ignoreFields (array ( 'password')); if ($ error = $ this-> session-> flashdata ('form_error')) echo "$ error"; echo form_open ('test / proc'); echo $ this-> Datagrid-> generate (); echo Datagrid :: createButton ('delete', 'Delete'); echo form_close (); functie proc ($ request_type = ") $ this-> load-> helper ('url'); if ($ action = Datagrid :: getPostAction ()) $ error = ""; switch ($ action) case 'delete': if (! $ this-> Datagrid-> deletePostSelection ()) $ error = 'Items kunnen niet worden verwijderd'; pauze; if ($ request_type! = 'ajax') $ this-> load-> library ('session'); $ This-> session-> set_flashdata ( 'form_error', $ fout); redirect ( 'test / index'); else echo json_encode (array ('error' => $ error)); else die ("Bad Request"); ?>
Een exemplaar van deze klasse is gemaakt en doorgegeven als een verwijzing naar de $ This-> Datagrid
lid. Merk op dat we gegevens zullen ophalen van een tabel genaamd "gebruikers" waarvan de primaire sleutel de "id" kolom is; Vervolgens nemen we bij de indexmethode de volgende stappen: configureer het Datagrid-object, geef het weer in een formulier met een verwijderknop eraan toegevoegd en kijk of alles werkt zoals verwacht:
Antwoord: De "Test :: proc ()
"methode zorgt voor de verwerking van het formulier en het kiezen van de juiste bewerking om tegen de ID kaart
s die zijn geselecteerd door de afzender van het formulier. Het zorgt ook voor AJAX-verzoeken, dus het echoot een JSON-object terug naar de client. Deze AJAX-bewuste functie komt van pas wanneer jQuery in actie komt, wat nu het geval is!
"Het is altijd een slim idee om webtoepassingen te maken die compenseert wanneer JavaScript / AJAX niet beschikbaar is. Op deze manier zullen sommige gebruikers een rijkere en snellere ervaring hebben, terwijl gebruikers zonder JavaScript de toepassing toch gewoon kunnen gebruiken."
Wanneer de gebruiker op de knop klikt (of op een andere actieknop), willen we misschien voorkomen dat de pagina opnieuw wordt geladen en opnieuw moet worden gegenereerd. hierdoor kan de gebruiker van onze applicatie in slaap vallen! Het omzeilen van dit probleem zal geen moeilijke taak zijn als we ons houden aan de jQuery-bibliotheek. Omdat dit geen zelfstudie is, ga ik niet door alle details met betrekking tot hoe u de bibliotheek kunt vinden, hoe u deze op de pagina kunt opnemen, enzovoort. Van u wordt verwacht dat u deze stappen alleen weet.
Maak een map met de naam "js
", voeg de jQuery-bibliotheek toe en maak een weergavebestand met de naam users.php
. Open dit nieuwe bestand en voeg toe:
Gebruikersbeheer Datagrid-> hidePkCol (true); if ($ error = $ this-> session-> flashdata ('form_error')) echo "$ error"; echo form_open ('test / proc', array ('class' => 'dg_form')); echo $ this-> Datagrid-> generate (); echo Datagrid :: createButton ('delete', 'Delete' ); echo form_close ();?>
Heb je je gerealiseerd dat we de code hebben verwijderd? Test :: index
en in het nieuwe view-script? Dit betekent dat we het moeten veranderen Test :: index ()
methode dienovereenkomstig:
function index () $ this-> load-> helper ('form'); $ This-> load-> library ( 'session'); $ This-> load-> weergave ( 'gebruikers');
Dat is beter. Als u wat opmaak aan het raster wilt toevoegen, kunt u de volgende CSS gebruiken (of zelf een betere opmaak maken):
.dg_form table border: 1px solid silver; .dg_form th background-color: grey; font-family: "Courier New", Courier, mono; font-size: 12px; .dg_form td background-color: gainsboro; font-size: 12px; .dg_form input [type = submit] margin-top: 2px;
Maak nu een "datagrid.js" -bestand aan, plaats het in de map "js" en begin met deze code:
$ (function () // coole dingen hier?)
In deze afsluiting schrijven we code die zal worden belast met het besturen van bepaalde verzendgebeurtenissen zodra de pagina volledig is geladen. Het eerste dat we moeten doen, is bijhouden wanneer een gebruiker op een verzendknop op het gegevensrasterformulier klikt en vervolgens die gegevens verzenden om op de server te worden verwerkt.
$ ('. dg_form: submit'). klik (functie (e) e.preventDefault (); var $ form = $ (this) .parents ('form'); var action_name = $ (this) .attr (' class '). replace ("dg_action_", ""); var action_control = $ (''); $ Form.append (action_control); var post_data = $ form.serialize (); action_control.remove (); var script = $ form.attr ('actie') + '/ ajax'; $ .post (script, post_data, function (resp) if (resp.error) alert (resp.error); else switch (action_name) case 'delete': // verwijder verwijderde rijen uit het raster $ formulier .find ('. dg_check_item: checked'). parents ('tr'). remove (); break; case 'anotherAction': // doe iets anders? break;, 'json'))
Als alternatief zouden we kunnen zijn begonnen met iets als: $ ('. dg_form'). submit (functie (e) ?)
. Aangezien ik echter wil bijhouden welke knop is ingedrukt en de naam van de gekozen actie op basis daarvan heb uitgepakt, geef ik er de voorkeur aan om een gebeurtenishandler aan de verzendknop zelf te binden en vervolgens de hiërarchie van knooppunten op te gaan om de vorm te vinden die de ingedrukte knop hoort bij:
// vindt het formulier var $ form = $ (this) .parents ('form'); // extraheert de naam van de actie var action_name = $ (this) .attr ('class'). replace ("dg_action_", "");
Vervolgens voegen we een verborgen invoerelement in het formulierelement toe om aan te geven welke actie wordt verzonden:
// maak de verborgen invoer var action_control = $ (''); // toevoegen aan het formulier $ form.append (action_control);
Dit is nodig omdat de functie de knop Verzenden niet als een geldig formulierinvoer beschouwt. Dus we moeten die hack op zijn plaats hebben bij het serialiseren van de formuliergegevens.
action_control.remove ();
"Vergeet niet: de functie negeert de submit-knop en wijst deze af als slechts een stukje markup-junk!"
Vervolgens gaan we verder met de actie
attribuut van het formulierelement en voeg de tekenreeks toe "/Ajax
"naar die url, dus de methode zal weten dat dit in feite een AJAX-verzoek is. Daarna gebruiken we de jQuery.post
functie om de te verwerken gegevens door de juiste controller, serverzijde, te verzenden en vervolgens de responsgebeurtenis te onderscheppen met een geregistreerde callback / sluiting:
? var script = $ form.attr ('actie') + '/ ajax'; $ .post (script, post_data, function (resp) if (resp.error) alert (resp.error); else switch (action_name) case 'delete': // verwijder verwijderde rijen uit het raster $ formulier .find ('. dg_check_item: checked'). parents ('tr'). remove (); break; case 'anotherAction': // doe iets anders? break;, 'json')
Merk op dat we de reactie vragen om gecodeerd te worden als "json", aangezien we die string doorgeven als het vierde argument van de $ .post
functie. De inhoud van de callback die te maken heeft met de serverreactie moet vrij eenvoudig te begrijpen zijn; het bepaalt of er een fout is en, zo ja, waarschuwt het. Anders wordt aangegeven dat de actie is verwerkt (in dit geval als het een actie "" is, verwijderen we de rijen die betrekking hebben op de actie. ID kaart
s die zijn geselecteerd door de gebruiker).
Het enige dat nu ontbreekt, is de schakelfunctie die ik eerder heb beloofd. We moeten een callback-functie registreren voor wanneer het selectievakje "Master" - waarbij een klassenkenmerk is ingesteld op "dg_check_toggler
"- wordt geklikt. Voeg het volgende codefragment toe na het vorige:
$ ('. dg_check_toggler'). klik (functie () var checkboxes = $ (this) .parents ('table'). find ('. dg_check_item'); if ($ (this) .is (': checked') )) checkboxes.attr ('checked', 'true'); else checkboxes.removeAttr ('checked');)
Wanneer op het aankruisvakje "toggle" wordt geklikt, als het naar een "checked" -status gaat, worden alle rijen van het bijbehorende datarooster gelijktijdig gecontroleerd; anders wordt alles uitgeschakeld.
We hebben nog niet het topje van de ijsberg bereikt als het gaat om gegevensrasters voor complexere contentbeheersystemen. Andere kenmerken die nuttig kunnen blijken zijn:
Bedankt voor het lezen. Als je een vervolgstudie wilt, laat het me dan weten in de reacties!