visit
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.
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";
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"
},
];
// utility function to write HTML colors
this._rgbaFromColorArray = function(c) {
return rgba(${255*c[0]}, ${255*c[1]}, ${255*c[2]}, ${c[3]});
}
// -- 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;
}
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);
}
}
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.
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
// //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;
}
this.defaultContentSizeInWebPixels = [500, 300];
this.hasFixedContentAspectRatio = false;