-
Notifications
You must be signed in to change notification settings - Fork 1
PluginStructure
A typical plugin has a more-or-less common structure, although (a) it evolved during the course of the original project, 2015–2018; (b) Tim often changed the naming conventions arbitrarily due to forgetting what they were; and (c) some plugins require different resources, such as connections to php
and mySQL
.
A plugin named foo
resides in /plugins/foo/
. Most of the code sits in that file, with other resources in subfolders such as art/
or data/
.
This is named foo.html
Its <head>
section contains a slew of includes, often looking something like this:
<link href='https://fonts.googleapis.com/css?family=Maven+Pro:700,900' rel='stylesheet'>
<link href='https://fonts.googleapis.com/css?family=Rokkitt:300, 700' rel='stylesheet'>
<link rel='stylesheet' type='text/css' href='foo.css'/>
<link rel='stylesheet' type='text/css' href='../common/jqueryFolder/jquery-ui.min.css'/>
<script src="../common/iframe-phone.js" language="javascript"></script>
<script src="../common/codapInterface.js" language="javascript"></script>
<script src="../common/pluginHelper.js" language="javascript"></script>
<script src="../common/TEEUtils.js" language="javascript"></script>
<script src="../common/jqueryFolder/jquery.min.js" language="javascript"></script>
<script src="../common/jqueryFolder/jquery-ui.min.js" language="javascript"></script>
<script src="foo.js" language="JavaScript"></script>
<script src="fooGameConfigurations.js" language="JavaScript"></script>
<script src="fooStrings.js" language="JavaScript"></script>
<script src="foophpConnector.js" language="JavaScript"></script>
<script src="fooCODAPConnector.js" language="JavaScript"></script>
<script src="fooUI.js" language="JavaScript"></script>
<script src="fooUserActions.js" language="JavaScript"></script>
<script src="fooModel.js" language="JavaScript"></script>
Note that there is usually a foo.css
to style the plugin.
This is a central site for js functionality. As Tim developed a prototype, this was often the first js file he made, and then, as things got more complex, functions and members were offloaded into other files. In a more mature plugin, it functions as a controller, but often also implements functionality that has not yet found a home.
It defines the top-level global named foo
; ideally it's the only one. So we often see:
let foo = { // top level global
state : {},
at the top of this file. Note that this is where foo.state
gets defined.
Sometimes the object foo.constants
gets its own file; sometimes it lives in foo.js
.
Sometimes fooCODAPConnect.js
or simply CODAPconnect.js
or other variants. In ay case, defines the object foo.CODAPConnect
or foo.connector
.
This file has all the routines we use to communicate with CODAP, particularly emitting data we have generated into the platform. Here is an example from fishCODAPConnector.js
:
createFishItems: async function (iValues) {
iValues = pluginHelper.arrayify(iValues);
console.log("Fish ... createFishItems with " + iValues.length + " case(s)");
try {
res = await pluginHelper.createItems(iValues, fish.constants.kFishDataSetName);
console.log("Resolving createFishItems() with " + JSON.stringify(res));
return res;
} catch {
console.log("Problem creating items using iValues = " + JSON.stringify(iValues));
}
},
In this example, we have finally gotten wise to asynchronous coding (note the async function
in the definition and the await
where we ask pluginHelper
to create the items). Earlier plugins do not have this; that's a good project to undertake one day.
If the plugin needs a database (we've used mySQL
during prototyping, but changing to something better would make sense), you will need php to communicate with it.
This file contains routines that set up the parameters for a specific task, and one method that actually makes the call. We are using the Fetch
system of communicating. Here are two routines from fish.phpConnector.js
for your perusal:
sendCommand: async function (iCommands) {
const theCommand = iCommands.c;
let theBody = new FormData();
for (let key in iCommands) {
if (iCommands.hasOwnProperty(key)) {
theBody.append(key, iCommands[key])
}
}
theBody.append("whence", fish.whence); // here is where the JS tells the PHP which server we're on.
let theRequest = new Request(
fish.constants.kBaseURL[fish.whence],
{method: 'POST', body: theBody, headers: new Headers()}
);
try {
const theResult = await fetch(theRequest); // here (finally) is the fetch!
if (theResult.ok) {
const theJSON = await theResult.json();
return theJSON;
} else {
console.error("sendCommand error: " + theResult.statusText);
}
}
catch (msg) {
console.log('fetch sequence error: ' + msg);
}
},
getGameData: async function () {
try {
const theCommands = {"c": "gameData", "gameCode": fish.state.gameCode};
const iData = await fish.phpConnector.sendCommand(theCommands);
return iData;
}
catch (msg) {
console.log('get game data error: ' + msg);
}
},
In this example, suppose some other routine needs game data from the DB. It calls await fish.phpConnector.getGameData()
. This is the bottom routine in the example. That function constructs the commands that php will need (they will be $_REQUEST
variables on the inside) in the object theCommands
. Then it asks sendCommand
to send them.
sendCommand
, for its part, does a little dance. It:
- translates the commands object into a
FormData
calledtheBody
; - adds an extra command,
whence
, which tells us what system we're on (e.g.,"local"
); - creates a
Request
object that includes the commands as well as the URL for the php file (which depends onwhence
as well); - finally performs the
fetch()
, awaits its completion (it's aPromise
), and returnstheResult
; - extracts the JSON version of
theResult
and returns that JSON.
If a pure model file makes sense, this is a good place to put it.