BajaScript is a subset of Niagara's functionality made available through the browser using JavaScript. While the basic Type system and Object Model come built-in, along with support for many of the Components and Simples provided with Niagara, you may want to extend BajaScript's capabilities further. One way to accomplish this is through the Type Extension API.
What are Type Extensions?
A Type Extension extends BajaScript itself to provide a JavaScript implementation of a Niagara Simple or Complex Type. It can provide additional functionality not provided by the built-in baja.Simple, baja.Struct, or baja.Component classes.
Why would I want to use a Type Extension?
Without a Type Extension, a Niagara object in the browser will provide only a minimal API:
- For Simples:
encodeToString(),decodeFromString(), etc. - For Structs:
get(),set(), etc. - For Components: Struct functions plus
add(),remove(), etc.
But if you have your own custom Niagara Types, you will probably want customized API associated with them in the browser. For example:
toString()should provide info specific to that Type- Getters and setters apart from existing Slots
- Logic functions to calculate data based on Slot values
- Provide custom Slot Facets
To accomplish these and more, Type Extensions are the way to go.
How do I use Type Extensions?
To make use of a Type Extension, you must complete the following:
- Create a JavaScript implementation of the Niagara Type.
- Create a Niagara Type that registers your JavaScript with the framework.
- Create an Agent registration that associates your JavaScript with the Niagara Type it implements.
Because all the pieces must be in place for the Type Extension to work, it can be tricky to implement each one in isolation. This guide will walk you through the process to show you the easiest path forward. We will implement a Simple first, because Simples are actually a bit trickier to implement than Complexes. The subsequent Complex example will then be a piece of cake.
Note: the JavaScript examples will use ES6 class syntax, and therefore presume you are using
grunt-niagaraversion 2 or higher. If you are usinggrunt-niagaraversion 1 and choose not to upgrade, no worries - Type Extensions work just fine using ES5 prototypes.
Implementing a Type Extension
For the example, we'll create an implementation of gx:Size. Size makes a great example because it demonstrates the benefits of having a Type Extension while still being quite simple, so it's easy to follow along in the development process. However, since a Type Extension for Size already exists, we'll make our own called DemoSize. This will be a BajaScript Type Extension that implements our own Niagara Simple, typeExtensionDemo:DemoSize. We won't look at the implementation for BDemoSize in this guide, but it's provided in the attached source.
To implement a Simple, the following four features are required:
encodeToString(): encodes the Simple instance to a string to be used when sending up to the Station.decodeFromString(): decodes a string encoding sent from a Station to an instance of your Simple in the browser.make(): constructs a new instance of your Simple.DEFAULT: the default instance of your Simple.
Let's begin by creating DemoSize.js. By convention we usually place Type Extension implementations in src/rc/baja, but they can go wherever you like. We'll add dummy implementations of the core Simple functions just to get the code running - they won't be correct, but without them, BajaScript can't even create an instance to test with.
define([ 'baja!' ], function (baja) {
'use strict';
class DemoSize extends baja.Simple {
encodeToString() { return ''; }
decodeFromString() { return new DemoSize(); }
make() { return new DemoSize(); }
static get DEFAULT() { return DEFAULT; }
}
const DEFAULT = new DemoSize();
return DemoSize;
});
And of course, DemoSizeSpec.js:
define([
'baja!',
'baja!typeExtensionDemo:DemoSize',
'nmodule/typeExtensionDemo/rc/baja/DemoSize' ], function (
baja,
types,
DemoSize) {
'use strict';
describe('nmodule/typeExtensionDemo/rc/baja/DemoSize', () => {
it('is registered on typeExtensionDemo:DemoSize', () => {
expect(baja.$('typeExtensionDemo:DemoSize')).toEqual(jasmine.any(DemoSize));
});
});
});
Take a look at the one spec (which is failing). What we want is when we instantiate our Niagara Type typeExtensionDemo:DemoSize in the browser, we get an instance of our own DemoSize class. But since we're missing the right configuration in Java, we don't - BajaScript just gives us a plain baja.Simple. Let's fix that!
To do this, we'll implement a Niagara Type which both extends BBajaScriptTypeExt and is an agent on BDemoSize:
@NiagaraType(agent = @AgentOn(types = "typeExtensionDemo:DemoSize"))
@NiagaraSingleton
public final class BDemoSizeTypeExt
extends BBajaScriptTypeExt
implements BIOffline
{
private BDemoSizeTypeExt() {}
@Override
public JsInfo getTypeExtJs(Context cx) { return JS_INFO; }
private static final JsInfo JS_INFO = JsInfo.make(
BOrd.make("module://typeExtensionDemo/rc/baja/DemoSize.js"),
BTypeExtensionDemoJsBuild.TYPE);
}
Take note of a couple things here.
- Our Type Extension implements
BIOffline. This marks it as being supported when using BajaScript in an offline context: that is, not talking to a running Station. For example, working with a .bog file in Workbench is an offline context. Without this marker interface, our Type Extension will not be used when offline. - We specify a
BJsBuildtype to indicate what minified JavaScript file our Type Extension code will get packaged into. For more details, see Building JavaScript Applications in Workbench Help.
We'll have to Slotomatic and rebuild to get our new class into the registry. Now our spec passes! Whenever we use BajaScript to instantiate a typeExtensionDemo:DemoSize, we'll get an instance of our DemoSize class. This includes accessing any Slot value from a Struct or Component, where the value is of type typeExtensionDemo:DemoSize! Now we can add whatever functionality we want to it, and it will be available throughout our application (or our customers' applications) when working with instances of our type.
class DemoSize extends baja.Simple {
constructor(width, height) {
super();
this.$width = width;
this.$height = height;
}
/**
* @returns {string}
*/
encodeToString() { return this.$width + ',' + this.$height; }
/**
* @param {string} str
* @returns {module:nmodule/typeExtensionDemo/rc/baja/DemoSize}
*/
decodeFromString(str) { return new DemoSize(...str.split(',').map(parseFloat)); }
/**
* @param {number} width
* @param {number} height
* @returns {module:nmodule/typeExtensionDemo/rc/baja/DemoSize}
*/
make(width, height) { return new DemoSize(width, height); }
/**
* @returns {number}
*/
getWidth() { return this.$width; }
/**
* @returns {number}
*/
getHeight() { return this.$height; }
/**
* @returns {string}
*/
toString() { return `Width: ${ this.$width } Height: ${ this.$height }`; }
/**
* @returns {module:nmodule/typeExtensionDemo/rc/baja/DemoSize}
*/
static get DEFAULT() { return DEFAULT; }
}
Compare this with the experience of working with DemoSize instances without a Type Extension. It would require the constant use of helper functions to parse the string encodings to usable numbers, and re-encode them when saving them to the station. Now we have a JavaScript class that encapsulates this behavior and represents the important functionality provided by the Java version. We can simply call getWidth()/getHeight() without thinking about parsing string encodings.
Type Extensions for Structs and Components work exactly the same way, but are even easier. Complexes don't require encodeToString(), decodeFromString(), etc. You can simply extend the appropriate type:
define([ 'baja!' ], function (baja) {
'use strict';
return class DemoComponent extends baja.Component {
toString() { return 'I am a DemoComponent!'; }
};
});
Demo Source Code
A full example module can be found in the dev/typeExtensionDemo folder of the downloaded Niagara image.