One of the best features of Neonto’s React Studio is the ability to make a plugin out of a component from an npm library. By doing this, you have access to a wide variety of high-quality code for just about any feature you can think of. Most of the time it is on the very easy end of the coding scale. In this tutorial we will create a bar chart plugin by wrapping the bar chart component from the popular [Recharts] library.
The plugin is a bundle of files with the extension .plugin. The file structure looks like this:
Contents ├── Info.plist ├── Executables │ ├── Main.js │ └── studio-ui.js* └── Resources ├── js │ └── mustache.js └── logo_raw.png*
The files with asterisks (*) are optional.
Info.plist is a holdover from Neonto’s origins on the Mac. It contains several values stored in Apple’s XML plist format. The only 2 you need to worry about are CFBundleName
(the name of your plugin) and CFBundleIndentifier
(a unique ID).
mustache.js is also unused (in React Studio). It is there because Neonto’s plugin format is language and framework independent, and a single plugin can contain code for several targets. For example in Native Studio (another Neonto product) plugins use mustache for iOS and Android code generation. React Studio uses template literals for inline templating instead.
Main.js is where all the action takes place. The entire plugin can be written in it. Or.. just like any other Javascript program, you can separate parts of the program out into other files for readability. In the example above this has been done with the code for the InspectorUI.
Whether using just Main.js, or separating the code into multiple files, there are 5 main sections in a React Studio plugin:
For our example, we will put everything into Main.js.
Every plugin should start with its name, a brief description, and a default name to be used for it when it is dragged onto the canvas. Let’s name ours Recharts Bar Chart. We will also add a description, default name.
Finally, we will tell React Studio that this plugin is of type element (for more info on types [read here]).
Start your Main.js with this code snippet:
// -- plugin info requested by host app --
this.describePlugin = function(id, lang) {
switch (id) {
case 'displayName':
return "Recharts Bar Chart";
case 'shortDisplayText':
return "Create bar charts using recharts npm library";
case 'defaultNameForNewInstance':
return "rechartsBar";
}
}
this.__pluginHostId = "com.neonto.studio.element";
The inspector UI is the rightmost pane in React Studio, and it is where you expose the “knobs” that you want to let the user “turn” on your plugin. We are going to use only 4 of the 15 different types of controls available ([read this] for information on all of them):
datasheet-picker Will be used to select the datasheet that holds the data to be charted
textinput Will be used twice. The first time to enter the name of the datasheet column that contains the labels for the x-axis, the second to enter the name of the datasheet column that contains the values for the bars (y-axis).
color-picker Will be used to pick the color of the bars
label Will be used to create some descriptive lies to help the user. Add the following code to your Main.js:
// -- inspector UI --
this.inspectorUIDefinition = [
{
"type": "label",
"text": "Choose a data sheet that provides the data to display:"
},
{
"type": "datasheet-picker",
"id": "linkedDataSheet",
"actionBinding": "this._onUIChange"
},
{
"type": "label",
"text": "Use this column from data sheet for bar names/categories:"
},
{
"type": "textinput",
"id": "linkedBarNames",
"actionBinding": "this._onUIChange",
},
{
"type": "label",
"text": "Use this column from data sheet for bar values:"
},
{
"type": "textinput",
"id": "linkedBarValues",
"actionBinding": "this._onUIChange",
},
{
"paddingTop": 20,
"type": "label",
"text": "Following settings affect the graph's look:"
},
{
"paddingTop": 20,
"type": "label",
"text": "Colors:"
},
{
"type": "color-picker",
"id": "baseColor",
"actionBinding": "this._onUIChange",
"label": "Column color"
},
];
The color-picker returns a color array that we need to convert to HTML rgba.
// utility function to write HTML colors
this._rgbaFromColorArray = function(c) {
return rgba(${255*c[0]}, ${255*c[1]}, ${255*c[2]}, ${c[3]});
}
If you wish to set any default values you can do so using this entry point:
// -- private variables --
// these are any default values we wish to set
this._data = {
linkedDataSheet: "",
linkedBarNames: "country",
linkedBarValues: "value",
};
// -- persistence, i.e. saving and loading --
this.persist = function() {
return this._data;
}
this.unpersist = function(data) {
this._data = data;
}
Now we need to add some code that will take these controls and draw them in the Inspector pane of React Studio. This code is always the same for all your plugins. There is no need to change it. It is commented to explain what each entry point does.
this._accessorForDataKey = function(key) {
// This method creates unique keys for each of the controls above
// Both onCreateUI and onUIChange (see below) will call this method.
var accessorsByControlType = {
'textinput': 'text',
'checkbox': 'checked',
'numberinput': 'numberValue',
'multibutton': 'numberValue',
'color-picker': 'rgbaArrayValue',
'element-picker': 'elementId',
'screen-picker': 'screenName',
'dataslot-picker': 'dataSlotName',
'datasheet-picker': 'dataSheetName'
}
var accessorsByControlId = {};
for (var control of this.inspectorUIDefinition) {
var prop = accessorsByControlType[control.type];
if (prop && control.id)
accessorsByControlId[control.id] = prop;
}
return accessorsByControlId[key];
}
this.onCreateUI = function() {
// Bind values in this._data (see below) to UI automatically
// using "_accessorForDataKey" above.
var ui = this.getUI();
for (var controlId in this._data) {
var prop = this._accessorForDataKey(controlId);
if (prop) ui.getChildById(controlId)[prop] = this._data[controlId];
}
}
this._onUIChange = function(controlId) {
// This will take the user entered values and bind them using "_accessorForDataKey"
var ui = this.getUI();
var prop = this._accessorForDataKey(controlId);
if (prop) {
console.log("updated: "+controlId);
this._data[controlId] = ui.getChildById(controlId)[prop];
} else {
console.log("** no data property found for controlId "+controlId);
}
}
We will skip over creating a preview for this tutorial. The plugin will work fine without it. We will cover preview generation in future tutorials.
This section would be used if we wanted to make some functionality in our plugin available to other elements in React Studio. For example a camera plugin would expose a “shoot” method that could be accessed as an Interaction by a Button element.
Now the fun part: code generation.
Let’s tell the React Studio design compiler that we are wrapping an existing component.
this.writesCustomReactWebComponent = false;
Because we declared this to be false, the design compiler will look for the next two entry points, this.getReactWebRenderMethodSetupCode
and this.getReactWebJSXCode
.
this.getReactWebRenderMethodSetupCode
will run once per render.
But before we get to that, we must first tell React Studio which npm package we want to use, and which components to import from it. If you go to the recharts simple bar chart code sandbox you will see this:
We will simplify it just a bit by removing the CartesianGrid
, Tooltip
, and Legend
components.
// -- code generation, React web --
// what react library(s) do we want?
this.getReactWebPackages = function() {
return {
"recharts": "^2.0.9"
};
}
// what React components do we want?
this.getReactWebImports = function(exporter) {
var arr = [
{ varName: "{ ResponsiveContainer, BarChart, Bar, XAxis, YAxis}", path: "recharts" }
];
return arr;
}
The most important thing to be aware of is that this entry point is implemented as a function, so we have to consider two “scopes of execution”: the first execution is happening at “compile time” when the design compiler reads the “outer” Javascript that culminates in the evaluation of the template literal in the return
statement The returned/generated code will be written to your web application. The second execution is when the code generated by the template literal is evaluated at runtime in the users browser.
// boilerplate code for wrappers, see
// https://docs.neonto.com/plugins/apiref/element.html#specific-to-reactjs-target
// This generated code will be called once per every render
this.getReactWebRenderMethodSetupCode = function(exporter, elementName) {
// grab the contents of the datasheet pointed at in this.inspectorUIDefinition
var dataSheetCode = exporter.valueAccessForDataSheetByName(this._data.linkedDataSheet);
// create the NAME of a variable dynamically
// it is based on the the name we gave the
//chart plugin when we dropped it on the canvas
const sheetVarName = `sheet_${elementName}`;
// stuff that name into property of the parent object
// this is so we can access it from within getReactWebJSXCode()
this._reactRenderDataVarName = sheetVarName;
// this is the template literal that will execute in the runtime environment.
// we create an array out of the items oject within the datasheet json object
return `const ${sheetVarName} = ${dataSheetCode}.items;`
}
Now we want to use this data, and also the constants entered into the Inspector UI, as props for the recharts barChart component. Again, note that we are writing some Javascript (var jsx
)that allows us to evaluate a template literal which is what is returned by the sun to the diagnosis compiler for execution in the user’s browser at runtime. Going back to the recharts code sandbox we see:
Because we removed the CartesianGrid
, Tooltip
, and Legend
imports, we must also remove the components from the return statement. We will also remove the margin
property of the BarChart
component just to clean things up a bit. We can set margins in react Studio.
this.getReactWebJSXCode = function(exporter) {
// The next 4 lines prepare text to be inserted into the template literal
// (for execution at runtime)
//grab the NAME of the variable that contains the data
var chartData = this._reactRenderDataVarName
// grab the actual values the have been entered into the Inspecter UI
var labels = this._data.linkedBarNames
var bars = this._data.linkedBarValues;
var color = this._rgbaFromColorArray(this._data.baseColor);
// prepare the JSX code by inserting the above values into the template literal
// this forms the main body of the render(return()) for the plugin/component
var jsx =
`
<ResponsiveContainer width="100%" height="100%">
<BarChart data={${chartData}}>
<XAxis dataKey="${labels}" axisLine={false} tickLine={0} />
<YAxis axisLine = {false} tickLine={0} />
<Bar dataKey="${bars}" fill="${color}" />
</BarChart>
</ResponsiveContainer>
`;
return jsx;
}
The last thing we need to do is to tell the design compiler what our default size is, and if that can be over-ridden by the layout controls of react Studio.
this.defaultContentSizeInWebPixels = [500, 300];
this.hasFixedContentAspectRatio = false;
Your plugin is now ready to use.
Create a datasheet and choose ”Add mock data -> Countries” and then ”Add mock data -> Random numbers (range 0 to 100)”. Then delete most of the rows (so that the chart is not over populated).
Now drag your plugin onto the canvas of your Start screen, and choose the datasheet you created from the datasheet picker. If you added the mock data as described above, you can leave the default common names as they are. Pick a nice color for the bars in the car chart.
Now click “Open in Web Browser”, and you will have a nice bar chart to look at.