Provide new ways to display and download the data.
People need data in different formats. The Core script includes a number of standard formats: CSV, Excel, HTML, XML, JSON, assorted programming language data structures and SQL. However, this list is far from exhaustive. This page explains how to add your own Export Types so you can use the Data Generator to generate the random data in whatever format you require.
Export Types appear in the main user interface and may contain whatever additional settings you may need. You can include an optional JS module that ties in with the interface, to do things like perform Export Type-specific validation on the Data Type rows selected, or really anything else you may need. On the backend, Export Types do the job of tying together all the data produced by the Data Types and generating whatever strings is needed for the entire data set. We'll step through how it all works in a bit, but let's start with dissecting an Export Type to see each part.
Now let's do a high-level view of what goes into a module: the files and folders, the JS + PHP components and how the translations / internationalization works. We'll get into the details about the code in the following sections.
All Export Types are found in the /resources/plugins/exportTypes/
folder. Each Export Type has its own folder,
which acts as the namespace for the JS and PHP code. What I mean is that the exact string you choose for the folder (like
XML
or CSV
) has to be used in your JS module creation and PHP class definition. I'll explain
all that below.
An Export Type has the following required files. Let's assume the folder name is MyNewExportFormat
.
/resources/plugins/exportType/MyNewExportFormat.js
: this file can actually be called whatever you want, but
for consistency and for keeping reading the Web Inspector / Firebug net panel, I'd name them like this. You can have
as many JS files as you want, but one is almost certainly enough./resources/plugins/exportType/MyNewExportFormat.class.php
: this contains your ExportType_MyNewExportFormat
class, which handles all necessary server-side code: the actual markup/export string generation and any markup you want available in the
generator webpage. More info about all that below./resources/plugins/exportType/lang/en.php
: A PHP file containing a single array (hash) that lists all
strings used in your module.You can also include any custom CSS files you want. See the PHP class definition below for more information.
The JS module for your Export Type does the following:
Manager
JS component, to allow it to publish and subscribe to messages; i.e.
to interact with the Core script and detect when certain user interface events happen.The PHP class for your Export Type handles the following functionality:
All text strings that appear in your module should be pulled from a language file. It's very simple. Just create a
file called en.php
in your /resources/plugins/exportTypes/[export type folder]/lang/
folder.
That file should contain a single $L
hash, like so:
<?php $L = array(); $L["EXPORT_TYPE_NAME"] = "XML"; $L["label1"] = "Label 1"; $L["label2"] = "Label 2"; // ...
Once you do that, the Data Generator automatically makes that information accessible to your PHP and JS code. I'll explain how that works in the following sections.
All plugins - Data Types
, Export Types
and Country
plugins have to extend
a base, abstract class defined by the core code. Hopefully you know what this means, but if not - time for some
Googling! Simply put, abstract classes are a mechanism to help ensure that the class being defined has a proper
footprint and contains all the functionality that's expected and required.
For Export Types, take a look at this file: /resources/classes/ExportTypePlugin.class.php
. That's the
class you'll need to extend.
Now let's look at an actual implementation. If you want to see the complete list of available variables and methods,
check out the source code of the Export Type abstract class (/resources/classes/ExportTypePlugin.abstract.class.php
).
It's well documented.
This is the PHP class for the CSV
class. It's a simple Export Type that outputs the randomly generated data in
CSV format. It provides the user with the option to choose the line ending char (Windows / Mac, Unix) and the
delimiter char. Pretty straightforward.
<?php /** * @package ExportTypes */ class CSV extends ExportTypePlugin { protected $isEnabled = true; protected $exportTypeName = "CSV"; protected $jsModules = array("CSV.js"); protected $contentTypeHeader = "application/csv"; public $L = array(); function generate($generator) { $exportTarget = $generator->getExportTarget(); $postData = $generator->getPostData(); $data = $generator->generateExportData(); $csvDelimiter = ($postData["etCSV_delimiter"] == '\t') ? "\t" : $postData["etCSV_delimiter"]; $csvLineEndings = $postData["etCSV_lineEndings"]; switch ($csvLineEndings) { case "Windows": $newline = "\r\n"; break; case "Unix": $newline = "\n"; break; case "Mac": default: $newline = "\r"; break; } $content = ""; if ($data["isFirstBatch"]) { $content .= implode($csvDelimiter, $data["colData"]); } foreach ($data["rowData"] as $row) { $content .= $newline . implode($csvDelimiter, $row); } return array( "success" => true, "content" => $content ); } /** * Used for constructing the filename of the filename when downloading. * @see ExportTypePlugin::getDownloadFilename() * @param Generator $generator * @return string */ function getDownloadFilename($generator) { $time = date("M-j-Y"); return "data{$time}.csv"; } function getAdditionalSettingsHTML() { $LANG = Core::$language->getCurrentLanguageStrings(); $html =<<< END <table cellspacing="0" cellpadding="0" width="100%"> <tr> <td width="50%"> <table cellspacing="2" cellpadding="0" width="100%"> <tr> <td width="160">{$this->L["delimiter_chars"]}</td> <td> <input type="text" size="2" name="etCSV_delimiter" id="etCSV_delimiter" value="|" /> </td> </tr> </table> </td> <td width="50%"> <table cellspacing="0" cellpadding="0" width="100%"> <tr> <td width="160">{$this->L["eol_char"]}</td> <td> <select name="etCSV_lineEndings" id="etCSV_lineEndings"> <option value="Windows">Windows</option> <option value="Unix">Unix</option> <option value="Mac">Mac</option> </select> </td> </tr> </table> </td> </tr> </table> END; return $html; } }
Let's look at each line in turn.
class CSV extends ExportTypePlugin
: our class definition. All Export Type class names
must be the same as the folder.$isEnabled
: this var explicitly enables/disables the module. In case you're tinkering around with
a new Export Type, sometimes you may not want it to show up in the UI - so you'd just set this to
false
.$exportTypeName
: this is the human-readable name of your module. It can be in whatever
language you want, but we prefer English as the default language string. The value you enter in this variable
is automatically overridden if the current selected language has the following key in the language file array:
$L["EXPORT_TYPE_NAME"] = "New Name";
This provides a simple mechanism to provide alternative translations
of your Export Type names.$jsModules
: an array containing whatever JS files you want to include. Note: these must be in
AMD format, for compatibility with requireJS.$contentTypeHeader
: this is used for the Prompt to download option. It lets your Export Type
send additional headers that let the user's operating system know how to handle the data format.$L
: this variable is automatically populated with the appropriate language strings when your class
is instantiated.Now onto the methods.
generate
: this is the main generation function of your class. It does the job of actually generating the
final output data. One important thing to know about Export Type generation is that is happens in different contexts.
For the in-page export type, the Export Type has to generate it in chunks - each chunk being (say) 100 rows at
a time. That information is sent back by the Core script via an Ajax call. As such, if the Export Type supports
that export type, the generate
function needs to be able to handle both scenarios: generating the final
result piece-meal, or in one big chunk. You can see how it works in the code above. It checks the $data["isFirstBatch"]
boolean to figure out whether to create the first row or not. Similarly, there's a $data["isLastBatch"]
key available as well (not used here).
getDownloadFilename
: returns the filename for the downloadable content, used in the Prompt to Download
export format.getAdditionalSettingsHTML
: this method returns whatever HTML you want to appear in the Export Type
tab. This is handy if you want to allow for a little fine-tuning of exactly what your Export Type generates. Look
at the other Export Types to get an idea of how this method can be used.Alright! Here's the full list of class vars that have special meaning.
Var | Req/Opt | Type | Explanation |
---|---|---|---|
$exportTypeName | required | string | The name of the export type "HTML", "XML" etc. This is always in English; even in different languages,
"JSON" is still "JSON", so having no translation is acceptable here. But if you do need it to be overridden
for different languages, add a $L["EXPORT_TYPE_NAME"] key to your lang files. That will override
this default value.
|
$isEnabled | optional | boolean | Used during development. Only Export Types that have $isEnabled == true will get listed in the Data Generator for use. |
$jsModules | optional | array | An array of JS modules that need to be included for this module. They should be requireJS-friendly modules. |
$cssFiles | optional | array | A single CSS file for any additional CSS needed for the module. It's up to the developer to properly name their CSS classes/IDs to prevent namespace collisions. |
$codeMirrorModes | optional | array | An array of whatever CodeMirror modes (the syntax highlighter) this Export Type needs. This ensures they're all loaded at runtime for use in the generator. |
$contentTypeHeader | optional | string | Needed for the "prompt for download" export option. This should contain the standard Content-Type header value (like "text/html") of the generated content, so the browser knows what to do with the downloaded file. |
$compatibleExportTypes | optional | array |
Export Types *should* be able to handle all three Export Targets available in the page (in-page, new window/tab, prompt for download), but if they
can't, they should specify this var. The system will automatically grey out those options that aren't selectable as soon as the user selects the
Export Type. Possible values in array: inPage , newTab , promptDownload (all strings).
|
$compatibleExportTypes | optional | array |
Export Types *should* be able to handle all three Export Targets available in the page (in-page, new window/tab, prompt for download), but if they
can't, they should specify this var. The system will automatically grey out those options that aren't selectable as soon as the user selects the
Export Type. Possible values in array: inPage , newTab , promptDownload (all strings).
|
$L | auto-generated | array | Do NOT define this variable. When your Export Type is instantiated, this variable is auto-generated and populated with the appropriate language file. |
Req/Opt | required |
---|---|
Params |
|
Explanation |
As mentioned above, this is the main generation function of your Export Type class. It's responsible for generating the
final outputted data. The generate function is required to handle all supported export types - i.e.
in-page, new window / tab and prompt for download. The difference is that for the in-page
export type, the Export Type has to generate it in chunks - each chunk being (say) 100 rows at a time. That
information is sent back by the Core script via an Ajax call. As such, if the Export Type supports that export type,
the generate function needs to be able to handle both scenarios: generating the final result piece-meal,
or in one big chunk. You can see how it works in the code above. It checks the $data["isFirstBatch"] boolean
to figure out whether to create the first row or not. Similarly, there's a $data["isLastBatch"] key available
as well (not used here).
|
Req/Opt | required |
---|---|
Params |
|
Explanation |
Req/Opt | optional |
---|---|
Params |
$runtimeContext: Export Types classes are instantiated at different times in the code. This parameter
is a string that describes the context in which it's being instantiated: ui / generation
|
Explanation |
An optional constructor. Note: this should always call parent::__construct($runtimeContext); .
|
Req/Opt | optional |
---|---|
Params | |
Explanation |
The following methods are defined on the Export Plugin abstract class, which you can use when developing your Export Type.
Function | Explanation |
---|---|
getName() | returns the Data Type name. |
getJSModules() | returns the array of JS modules. |
getCSSFiles() | returns the array of CSS files for the Data Type. |
getFolder() | returns the Export Type folder. |
getPath() | returns the path to the Export Type folder. |
getContentTypeHeader() | returns the content type header specified in the $contentTypeHeader member variable. |
getCodeMirrorModes() | returns the $codeMirrorModes member variable value. |
isEnabled() | returns whether or not the Data Type is enabled or not. |
Each Data Type may choose to have an optional JS component: a javascript module that performs certain functionality like saving/loading the data type data, running client-side validation on the user inputs (if required) and triggering whatever additional JS code is necessary.
The JS module is optional. The Core script handles saving and loading the Column Title and Data Type for all Data Types, so if you don't need anything in the Example or Options columns, you don't need to include a JS module.
Explaining how the JS module works can be a little abstract, so let's start with an example.
The following is the JS module for the Alphanumeric
Data Type. Give it a look over, then we'll
pull it apart and explain each bit below.
/*global $:false*/ define([ "manager", "constants", "lang", "generator" ], function(manager, C, L, generator) { "use strict"; /** * @name CSV * @see ExportType * @description Client-side code for the CSV Export Type. * @namespace */ var MODULE_ID = "export-type-CSV"; var LANG = L.exportTypePlugins.CSV; var _loadSettings = function(settings) { $("#etCSV_delimiter").val(settings.delimiter); $("#etCSV_lineEndings").val(settings.eol); }; var _saveSettings = function() { return { "delimiter": $("#etCSV_delimiter").val(), "eol": $("#etCSV_lineEndings").val() }; }; var _resetSettings = function() { $("#etCSV_delimiter").val("|"); $("#etCSV_lineEndings").val("Windows"); }; var _validate = function() { var delimiterField = $("#etCSV_delimiter"); var errors = []; // note we don't trim it. I figure whitespace could, technically be used as a delimiter if (delimiterField.val() === "") { errors.push({ els: delimiterField, error: LANG.validation_no_delimiter }); } return errors; }; manager.registerExportType(MODULE_ID, { validate: _validate, loadSettings: _loadSettings, saveSettings: _saveSettings, resetSettings: _resetSettings }); });
Now let's go line by line.
/*global $:false*/
this first line is for jshint/jslint. In my local environment, I use jshint with strict mode
to catch problems. This line just tells the interpreter to ignore the jQuery dollar sign global.define([ "manager", "constants", "lang", "generator" ], function(manager, C, L, generator) { //... });
The outer code that wraps the entire JS module is called within requireJS's /resources/scripts/requireConfig.js
. Each of those
discrete modules is in turn passed to the Data Type module via functions in the anonymous section param to define().
Whatever public API those modules reveal are now accessible via the four params: manager
, constants
,
lang
, generator
.
When defining your own Export Type module JS file, you'll want to include all four of those params. They all contain useful functionality and data that you'll need.
"use strict";
- do it! JS strict mode is never a bad idea. :Dmanager.registerExportType(MODULE_ID, { validate: _validate, loadSettings: _loadSettings, saveSettings: _saveSettings, resetSettings: _resetSettings });
This chunk of code is required for your Export Type JS Module. It registers your Export Type with the Core. That
allows it to listen to published events, publish its own events for other code to listen to, tie into the validation
functionality and so on. It's pretty straightforward. The manager.registerExportType()
function takes
two parameters: the unique MODULE_ID constant, (see below) and an object containing certain required
and optional functions, whose property names have special values. Again, more on that below. Now let's go back to the
top of the code again.
/** * @name CSV * @see ExportType * @description Client-side code for the CSV Export Type. * @namespace */ var MODULE_ID = "export-type-CSV"; var LANG = L.exportTypePlugins.CSV;
MODULE_ID
variable is special. It must always be of the form data-type-[FOLDER NAME].
That acts a unique identifier within the client-side code so the Manager can keep track of who's who.
L
function param fed to your Export Type contains all language
strings in the system - in whatever language is currently selected. To locate the strings for your own module,
just reference it by your Export Type folder name, again: L.exportTypePlugins.[FOLDER NAME]
As explained above, the second parameter of the manager.registerExportType()
function is an object
containing various predefined functions. This explains what are the properties for that object and what they're used
for. Note: all properties are optional, but you'll almost certainly need one or more.
Property | Params | Returns | Explanation |
---|---|---|---|
init | — | — | If this is defined for your Export Type, it gets called on page load prior to any events being published. By "event" I mean a custom published event, which I'll explain more thoroughly in the Pub/Sub section below. |
run | — | — | The run() function gets called for all Data Types and Export Types after their init()'s are called. As such, run() can rely on all subscriptions being in place so events published at this juncture will have an audience. |
saveSettings | — | object | When the user saves a data set, the Data Generator examines all Export Types - even those that aren't selected - and calls their saveSettings() method. This method is responsible for determining what information it wants to save for the row. Generally all it does is examine the DOM and extract whatever values the user entered in custom fields that the Export Type aded. It then returns an object of simple property-value pairs. |
loadSettings | data object | — | When a user loads a data set, each Export Type has their loadSettings() function called, with whatever previous saved information passed as the single parameter. |
validate | — | array |
This function needs to return an array of errors to display - or an empty array if there are no errors. Each
array index is an object of the following form: Check out the CSV Data Type's validate() function above for an example of how this function can work. |
As mentioned elsewhere, the client-side code revolves around the idea of publish/subscribe - or pub/sub. Different parts of the script can publish arbitrary events with arbitrary information associated with them, and any module can choose to listen out for particular events and run code when they occur. This is a very elegant pattern: it allow us to keep our modules loosely coupled and reduce the likelihood of introducing dependencies that can break things.
The core script publishes the following script for certain events that occur in the lifetime of the page. They're all
found in /resources/scripts/constants.php
(returned as JS). You can refer to them in your code via the
C
parameter, mapping to the constants
module. The names are pretty descriptive so I won't
bother explaining them any further.
C.EVENT.RESULT_TYPE.CHANGE
C.EVENT.COUNTRIES.CHANGE
C.EVENT.DATA_TABLE.ONLOAD_READY
C.EVENT.DATA_TABLE.ROW.CHECK_TO_DELETE
C.EVENT.DATA_TABLE.ROW.UNCHECK_TO_DELETE
C.EVENT.DATA_TABLE.ROW.DELETE
C.EVENT.DATA_TABLE.ROW.TYPE_CHANGE
C.EVENT.DATA_TABLE.ROW.EXAMPLE_CHANGE
C.EVENT.DATA_TABLE.ROW.ADD
C.EVENT.DATA_TABLE.ROW.RE_SORT
C.EVENT.DATA_TABLE.ROW.HELP_DIALOG_OPEN
C.EVENT.DATA_TABLE.ROW.HELP_DIALOG_CLOSE
C.EVENT.DATA_TABLE.CLEAR
C.EVENT.GENERATE
C.EVENT.IO.SAVE
C.EVENT.IO.LOAD
C.EVENT.TAB.CHANGE
C.EVENT.MODULE.REGISTER
C.EVENT.MODULE.UNREGISTER
Generally you'll want to set up your subscriptions in your module's init() function. Here's how it works:
... var _init = function() { var subscriptions = {}; subscriptions[C.EVENT.COUNTRIES.CHANGE] = _onChangeCountries; manager.subscribe(subscriptions); }; var _onChangeCountries = function(msg) { console.log(msg); }; ... manager.registerDataType(MODULE_ID, { init: _init }); ...
That would subscribe to the C.EVENT.COUNTRIES.CHANGE
event (which is where the user adds/removes a country
from the Country List section in the UI) and attaches a callback function - _onChangeCountries()
. The manager.subscribe()
function can be called at any time in any of your functions, so you can subscribe to events on the fly.
There are several client-side code libraries already available in the page that can be used in your Data Type:
You can always include additional libraries should you wish, but do try to namespace them.
When you add a new Export Type, just creating the new files and folders won't get it to show up in the UI. First, you'll need to follow the steps below to make sure your PHP class and (optionally) JS Module have been created properly, and afterwards you'll need to refresh the UI.
To update the list of available Export Types in the UI, go to the second Settings
tab. There, click the
Reset Plugins
button. A dialog will appears which resets all the available plugins (don't worry, this
won't cause any problems with saved content or anything like that). After refreshing the page, you should see
your Export Type appear as a tab at the bottom of the page.
If you feel that your Export Type could be of use to other people, send it my way! I'd love to take a look at it, and maybe even include it in the core script for others to download. Read the How to Contribute page.