Advanced Framework Customisation

The Tropofy framework provides widgets and capabilities to deal with the majority of scenarios, but for some special applications, extra functionality and flexibility is needed. This article discusses how to make use of additional Tropofy features that are rarely needed.

Custom Widgets

Custom widgets allow Tropofy users to write arbitrary HTML, JavaScript, and Python, and have it displayed like any other Tropofy widget. If a widget doesn’t already exist, implementing it as a custom widget is an option for adding the desired functionality to your Tropofy app.

All Tropofy widgets, including custom widgets, make use of the Knockout JavaScript framework, with each widget being a Knockout “component”. Your arbitrary HTML and JavaScript is treated as the template and ViewModel (respectively) for a Knockout component. For more information on these behaviours, see the Knockout component documentation.

The JavaScript for your custom widget is loaded via Asynchronous Module Definition (AMD) using Require.js. This means that you can load any other libraries you’d like using Require.js. Tropofy, by default, already provides:

  • jQuery 1.11.1 as jquery
  • Knockout.js 3.3.0 as knockout
  • Lo-Dash 2.4.1 as lodash
  • Leaflet.js 0.7.3 as leaflet

You can also make use of the Require.js text plugin.

Example

Below is the content required to make a very simple custom widget that just displays the current time using JavaScript.

The Python for the custom widget, in this case, simply points to the HTML and JavaScript files.

class ClockWidget(CustomWidget):
    def serialise(self, app_session):
        """
        These paths are relative to the static path from `App.get_static_content_path`

        You can include custom configuration in this dictionary, and it will be passed to params.spec in the
        JavaScript.
        """
        return {
            "js": "clock-widget.js",
            "html": "clock-widget.html"
        }

The ViewModel for the clock widget uses require.js to load moment.js and Knockout. It then defines time and stringTime for use in the template.

// Uses require.js to load moment.js and knockout
define(["moment", "knockout"], function(moment, ko){
    return function clockViewModel(params){
        /*
        params = {
            spec: data included in the widget's serialise method,
            step: {
                parent: see section on scaffold,
                widgets,
                stepGroup,
            }
        }
        */
        this.time = ko.observable(moment());
        this.stringTime = ko.computed(function(){
            // Time formatted as 11:39:44 am
            return this.time().format("h:mm:ss a");
        });

        function updateTime(){
            // Set this.time to moment(), which is now
            this.time(moment());
        }

        // update the time once per second
        setInterval(updateTime, 1000);

        return this;
    }
});

The HTML template for the Knockout component can make use of anything that the viewmodel defines in JavaScript, for example the this.stringTime computed property.

<div id="clock-widget-container">
    <p data-bind="text: stringTime"></p>
</div>

While this is a fairly trivial example, it demonstrates clearly how the different parts interact. If parts are unclear, consider reading the Knockout component documentation or continue reading, as the other advanced customisation features make use of more custom widgets in their examples.

Application-level Tick

For applications that require real-time communication between the server and client, Tropofy has a (disabled-by-default) application level “tick” built into the framework. None of our normal widgets currently use this, but it can be easily made use of in custom widgets via params.spec.tickData, which is just a reference to params.spec.step.parent.tickData.

The data in the tick is a JSON blob, generated by the tropofy.app.App.get_tic() function. Below is an application that includes a definition of tropofy.app.App.get_tic(), followed by the tick data it would produce.

class AppWithTick(App):
    def get_tic(self, app_session):
        num = self.sync_data.get("num", 0) + 1
        self.sync_data["num"] = num

        return {"msg": "hello!"}
{"serverTime": 2015-12-21 14:05:10,
"widgets": {}},
"syncData": {"num":4},
"app": {"msg":"hello!"}}

The result of tropofy.app.App.get_tic() is sent under “app”, and the sync_data object on App is sent as syncData. The special property of sync data is that the client will always send back the last sync data received, or an empty object/dict.

Custom App Javascript

Applications can include their own JavaScript to apply to the entire Tropofy app. This can be used to do anything, but one common use for this is sharing state between widgets that use the same data. Normally, widgets will have to maintain their data independently, even if the data is the same, but app-level JS can be used to share this data and have each widget maintain references.

The method for including application-level JavaScript can be seen below.

class CustomJSApp(App):
    def get_js_options(self, app_session):
        """
        Returns a dictionary containing the path to the application JavaScript file,
        relative to the static path from `App.get_static_content_path`
        """
        return {"app": "app.js"}

Accessing the app-level data object can be done in custom widget JavaScript via params.step.parent.app. An example app.js is included below

// Again, using AMD
define([], function(){
    return function(scaffold){
        // Application level Javascript is included by calling a
        // function on the scaffold.

        var obj = {}; // can be used for shared state

        scaffold.tickData.subscribe(function(data){
            // The application level tick can be accessed like this
            /* The subscribe method will call the given callback every
               time the tick data is updated */
            do_something();
        });

        return obj;
    }
})

The Scaffold

The core of the client-side JavaScript in the Tropofy framework is the scaffold. The scaffold is passed into the application-level JavaScript constructor, and through params.step.parent to custom widgets, and it is useful to know what it provides.

  • constructAppUrl(path) -> /appUrlName{path}
  • constructDataSetUrl(path) -> /appUrlName{path}/{dataSetId}
  • canAccessServer() -> true/false
  • selectedStep() -> {widgets, widgetIds, stepGroup}
  • selectStep(step)
  • selectFirstStep()
  • goToNextStep
  • goToPrevStep
  • logout
  • exportExcelTemplate
  • exportDataSet(dataSetId)
  • startAppWithDataSet(datSetId)
  • ajax(data) -> like jQuery.ajax but automatically retries, and calls .progress on each failed request
  • tickData() -> as in Application-level tick
  • syncData() -> as above
  • app -> application-level JS return value