Widget Development Guide

This guide describes how to build a QEDWiki widget.

Basic Widget Structure

This guide will break down the structure of a QED widget and go over where functionality can be added.

You begin your widget by including the required files:
require_once('Wiki.php');
require_once('WikiCommandWithMetadata.php');
require_once('WikiParameter.php');
You can also import any other files you may need to use. Note that when including a file, the root is the root of your QEDWiki installation.

Each QED widget has a package which it belongs to. The package name is included in the foldername and the filename for the wiget.


Example: The folder name will look like: cit_MyWidget_1_0_0

cit - This is the package name.
MyWiget - This is the name of the widget.
1_0_0 - This is the version of the file.. i.e. "1.0.0"

Inside of this folder you will have a your widget file. This file must have the same name as the folder and a .php extension.

Example: cit_MyWidget_1_0_0.php
You must name your QED widget in a specific way when creating the class. The file for this widget may be cit_MyWidget_1_0_0.php your
class declaration would be:
class cit_MyWidget_1_0_0 extends WikiCommandWithMetadata
{
Note that you must extend the WikiCommandWithMetadata when making most widgets.

Next you must declare constant values. These are used by QEDWiki:
const NAME          = 'Example';
const VERSION = '1.0.0';
const DESCRIPTION = 'This is an example command';
const CATEGORY = 'Data Access';
This defines the widget name as "Example" and puts it in the category "Data Access".

You also must define the events your widget will send to the datahub. This is done with:

public static $TOPICS  = array(DataHubEvent::AVAILABLE);
This would mean your widget creates an available event. If you had available, clicked and myalarm events you would have:
public static $TOPICS  = array(DataHubEvent::AVAILABLE, DataHubEvent::CLICKED, 'myalarm');
QEDWiki's DataHubEvent class has several predefined events.

There are several methods which you must include in your widget, the first is the constructor:

public function __construct($source = null, $parentNode = null) { parent::__construct($source, $parentNode); $this->_allowEdit = true ;

    $this->setAllowEdit(true);
$this->setVisual(WikiCommandWithMetadata :: NO_VISUAL);
$this->addParameter('id', 'Id', 'Y', 'id of command',
WikiParameter::STRING );
$this->addParameter('data', 'Data Source', 'N', 'Data access',
WikiParameter :: STRING + WikiParameter :: BINDABLE );
$this->addParameter('query', 'Query', 'N', 'Query to build from data source',
WikiParameter::STRING );
}
The $this->setVisual(WikiCommandWithMetadata :: NO_VISUAL); line sets the widget to be invisible, if it is not added then the widget defaults to visible.

The $this->addParameter() call adds the desired options into your widgets properties window. You can specify the WikiParameter to be a String, Integer, as well as bindable. Bindable means that the option will have a drop down allowing the user to bind to another event. If you wanted to start with a default value in you would do something like: $this->addParameter('id', 'Id', 'N', 'id of command', WikiParameter::STRING, '1'); . If you wanted a drop down list of options it would look like this: $this->addParameter('id', 'Id', 'N', 'id of command', WikiParameter::STRING, '1', '1,2,3' );

The getSample() method is used to return the wikicode to create your widget when it is shown in listplugin.php and when it is dropped onto a page

Notice! We have included _package="cit". This is required for any widgets which have a package specified other than the "qed" default package name. If you do not include this, you will get errors!

public function getSample() {
    $text = '{{Example id="example" _package="cit" /}}';
return $text;
}

In this function you can specify whatever starting values you want for the widget's options.

The getDragContent() function returns the content to display when dragging from the pallete

public function getDragContent() {
    return '<div style="border:solid;"><center><img src="'
    . $this->getIcon()
    . '!
' . $this->getName() . '</center>
'; } The init() function is run the first time the widget is called. This can be used to init any variables you use and to register your widget on the datahub as a producer/consumer.
public function init() {
    logit('Example::init()');
elogit('Example::init()');
// Init and Validate parameters $id = $this->getArgument('id');
if (!$id) { $id = nextid('id');
$this->setArgument('id', $id);
} // Identify ourselves as producer $this->_datahub->addProducer($this); // Listen for an event and define where to go when found $this->_datahub->addConsumer($id, $id . '/' . DataHubEvent::CLICKED,
array($this, 'bindToCallback'), null, false, true);
parent::init();
return true;
}

logit and elogit can be used to log which parts of your widget gets executed. The output from these goes into the log files in your qed folder, they are not displayed in the debug window.

In the above init function, $this->_datahub->addProducer($this); is used to identify the widget as a producer. This lets the datahub know that it will be passing data to it. We can also specify that we want to listen for events on the datahub with $this->_datahub->addConsumer(). In the above example, the widget is listening for a CLICKED event from itself. When there is a clicked event it will send that data to a method in our widget called bindToCallback.

The bindToCallback method would look something like this:

public function bindToCallback($event) {
        $values = $event->value;
        foreach ($values as $producer => $value) { ... }
        do some work
...
}

This is a convenient way of listening for events and when an event is found, processing it.

Instead of listening for an event from itself, this widget could be listening to events from other widgets and of other types. There are three types of Events: AVAILABLE, CLICKED, SELECTED.

The invoke() function is called every time the widget is loaded.

public function invoke() {
 
}

If a widget is bounded to an event and that event produces some data, the invoke function for the widget's that are listening will be run.

The renderBody() function is where you can display html and javascript to the user. This is the only function you can affect what is rendered on the page. It is also the only method that you CANNOT post to the datahub from php.

public function renderBody() {
 
}

This is executed like the invoke method.

Here is the completed skeleton code:

<?php

require_once('Wiki.php');
require_once('WikiCommandWithMetadata.php');
require_once('WikiParameter.php');

class cit_MyWidget_1_0_0 extends WikiCommandWithMetadata
{ const NAME = 'Example';
const VERSION = '1.0.0';
const DESCRIPTION = 'This is an example command';
const CATEGORY = 'Data Access';
public static $TOPICS = array(DataHubEvent::AVAILABLE, DataHubEvent::CLICKED);

/**
* Calls the parent constructor, defines the parameters for the
* plugin, and sets the category for the palette.
*
* @param string $id source id
* @param object $parentNode parentNode
* @return void no return
*/
public function __construct($source = null, $parentNode = null) { parent::__construct($source, $parentNode); $this->_allowEdit = true ;
$this->setAllowEditRecurse(true);
$this->setVisual(WikiCommandWithMetadata :: NO_VISUAL);
$this->addParameter('id', 'Id', 'N', 'id of command',
WikiParameter::STRING );
$this->addParameter('data', 'Data Source', 'N', 'Data access',
WikiParameter :: STRING + WikiParameter :: BINDABLE );
$this->addParameter('query', 'Query', 'N', 'Query to build from data source',
WikiParameter::STRING );
} /**
* Returns a sample of the plugin that can be run when the
* plugin is shown in listplugin.php.
*
* @return string Sample call of plugin
*/
public function getSample() { $sampleid = nextid('Example');
$text = '{{Example id="' . $sampleid . '" _package="cit" /}}';
return $text;
} /**
* Returns the content to display when dragging from the palette.
*
* @return string Drag content
*/
public function getDragContent() { return '<div style="border:solid;"><center><img src="' . $this->getIcon() . '! ' . $this->getName() . '</center>'; } /**
* Init function
*
* Obtains the id for the object and identifies the object as
* a producer on the datahub. Then, calls the parent init function.
* Finally, returns true.
*
* @return boolean Always true
*/
public function init() { logit('Example::init()');
elogit('Example::init()');
// Init and Validate parameters $id = $this->getArgument('id');
if (!$id) { $id = nextid('id');
$this->setArgument('id', $id);
} // Identify ourselves as producer $this->_datahub->addProducer($this); // Listen for an event and define where to go when found $this->_datahub->addConsumer($id, $id . '/' . DataHubEvent::CLICKED,
array($this, 'bindToCallback'), null, false, true);
parent::init();
return true;
} /**
* Invoke instantiates the _result object as WikiResultArray
*
* @return void No return
*/
public function invoke() { logit('Example::invoke()');
elogit('Example::invoke()');
} public function renderBody() { logit('Example::renderBody()');
elogit('Example::renderBody()');
} public function bindToCallback($event) { $values = $event->value; foreach ($values as $producer => $value) { ... } do some work
...
} }

Aside from the bindToCallback method that is added, the above is the bare minimum needed for a widget. The construction is easy, now to make it work.

Widgets work by putting data onto the datahub and getting data off the datahub. The most common object on the datahub is a WikiResultArray. You can create one by:

$this->_result = new WikiResultArray();

You must set each row of the array individually through something like:

$this->_result->setValue($data, 0);

This sets row 0 in the WikiResultArray to the contents of $data. If you were going to bind a Show Data to this you would want $data to be a 1 Dimensional associative array, associative because the array key's will be used for the column headers. Alternatively, $data could be an N dimensional array but whatever widgets bound to this would need to expect this and know what to do with it.

Once you have built up a WikiResultArray with some data and you want to pass it onto the datahub for other widgets to use you would do:

$evt = new DataHubEvent($id . '/' . DataHubEvent::AVAILABLE, $id, &#38;$this->_result);
$this->_datahub->fireEvent($evt);

This will make whatever you have in $this->_result available to any widget listening for that widget's AVAILABLE event. This can be executed from any method inside your widget EXCEPT renderBody() (as of qed 1.1).

If you want to put data onto the datahub from within your renderBody method you must do it inside of javascript:

<script language='JavaScript' type='text/javascript'>
var v = new Object();
v["application"] = "QED";
var evt = new DataHubEvent('<?php echo $id; ?>/<?php echo DataHubEvent::AVAILABLE; ?>', '<?php echo $id; ?>');
evt.value = v;
hub.fireServerEvent(evt);
</script>

This will put the contents of the object v onto the datahub. You can only do this inside of renderBody because you cannot use html or javascript in any other method.

Some Helpful Functions

Making REST calls in QEDWiki

To make REST calls in our QED widgets, we use CURL to make the request. The method is very similar to what you would do in a normal PHP application, except this time we want to store the data into QED's WikiResultArray. In the example below, we are making the call and we get XML data returned to us. Then, we go through the XML and extract the information that we need, and store it into an array. Then, we can take this array and store it into the WikiResultArray (as described above). Note: This step is not shown. Once, we get the data into the WikiResultArray, this data is then available to any of the other widgets on the page.

Here is the example:

$request = 'http://api.local.yahoo.com/MapsService/V1/geocode?appid=' . $this->getPluginData(self::KEY) . '&#38;location=' .urlencode($station['location']);            
$session = curl_init($request);
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
// Execute our request, and store the XML response $response = curl_exec($session); curl_close($session); $xml = new DomDocument;
$xml->loadXML($response);
$tableElement = $xml->getElementsByTagName('Result');
$myArray = array();
// Traverse through our XML and store the information into our array. foreach($tableElement as $nodeA) {
foreach($nodeA->childNodes as $nodeB) {
$name = strtolower($nodeB->nodeName);
$myArray["{$name}"] = $nodeB->nodeValue;
}
} // Then add $myArray to the WikiResultArray.
 

Tips and Best Practices

<<>>When debugging your widget, it is helpful to use the PHP error_log. You can enabled your error log by going into your php.ini file and turn "log error" On and uncomment the line which logs errors to a file. Now, you have all of your PHP errors going to a specific log file. So, how do you write to the error log from QED?>
$myName = "John Smith";
$myHobby = "football";
//Write this to the error log error_log("Hi, my name is" . $myName . " and I like " . $myHobby . ".");

//You can also view the contents of the WikiResultArray by printing it to your log file. $this->_result = new WikiResultArray();
$this->_result->setValue($data, 0);
error_log(print_r($this->_result, true));

Events in QEDWiki Widgets can be described as TOPICS and SUBTOPICS. An event topic is an event that your widget widget will publish to the datahub  in order to notify other widgets of a specific interaction of event. A subtopic pertains to those data elements that become available for a specific event.

So for example, if a widget performed some processing on data and wanted to notifiy that the processing was complete then the widget could publish a COMPUTED event to notify other widgets that it has refreshed its computations. Conversly, if your widget used a form and gathered input data from teh user the widget may trigger an AVAILABLE event that would need to provide bindable access to the various form values entered by the user. In this case the widget would specify a set of subtopics on the AVAILABLE event.

const GEOPOINTLNG = 'Longitude';
const GEOPOINTLAT = 'Latitude';
public static $TOPICS = array(DataHubEvent::AVAILABLE);
public static $SUBTOPICS = array(self::GEOPOINTLNG,self::GEOPOINTLNG);

public function init() {

// Identify ourselves as producer
$this->_datahub->addProducer($this);

// Notify Data Hub of the Topics/Subtopics we will produce.
$this->_datahub->addTopic($id.'/'.DataHubEvent::AVAILABLE);
foreach (self::$SUBTOPICS as $item) {
logit(self::TAG.": init() - Register Event Subtopic: Available[".$item."]");
elogit(self::TAG.": init() - Register Event Subtopic: Available[".$item."]");
$this->_datahub->addTopic($id.'/'.DataHubEvent::AVAILABLE.'['.$item.']');
}

}

In this example we have a widget that will publish two subtopics (Longitude, Latitude) on the Available event. The init() method is used to initialize the event publication structure to the DataHub.

Subtopics must always be enclosed within brackets such as "[Longitude]".

The QEDWiki framework expects each Widget class to have the following constants. Widgets go not need to specify get methods for these properties since the widget inherits the method implementations that do some groovy PHP tricks to read the constants.
const NAME          = 'Example';
const VERSION = '1.0.0';
const DESCRIPTION = 'This is an example command';
const CATEGORY = 'Data Access';

Widgets in QEDWiki must adhere to a small set of framework methods. The processing of a Widget within a WikiPage follows a distinct order of execution.

The following ordered list of methods describes the purpos eof a method in your widget and when it will be processed with respect to other methods:
  1.  __constructor() creates empty object with parameters defined;
  2. init() registers bus events widget produces and acquires external resources;
  3. discover() adds consumers of bus events and peer-to-peer event listeners (which may require searching fully init()-ed siblings);
  4. invoke() gets called when all arguments are satisfied, intended to run your business logic. It is only called when all the required inputs become valid or are changed.
  5. renderBody() creates your objects view, drawing on parameters and results of business logic from invoke.

Since widgets typically encapsulate external web services that may require authorization and authentication credentials, a widget should decouple such credentials from the operation of the widget. This way each user of the widget can specify their own credentials for externals services at the page or framework levels.  To accomplish this, a Widget should override the configure() method which will present a form for credentials on the Credentials tab of the Widget Editor. Examples of this practice can be seen in the following widgets: