paint-brush
Creating a React Studio Plugin From an npm Library - a Walk in The Park!by@azw
222 reads

Creating a React Studio Plugin From an npm Library - a Walk in The Park!

by Adam Zachary WassermanAugust 23rd, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Neonto’s React Studio lets you make a plugin out of a component from an npm library. In this tutorial we will create a bar chart plugin by wrapping the [Recharts] library. The plugin is a bundle of files with the extension plugin extension. The file structure looks like this: Info.plist, Executables, Main.js, Mustache.js and mustache.js. A single plugin can contain code for several targets. A plugin can start with its name, a brief description and a default name to be used for it when it is dragged onto the canvas.

People Mentioned

Mention Thumbnail

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - Creating a React Studio Plugin From an npm Library - a Walk in The Park!
Adam Zachary Wasserman HackerNoon profile picture

Wrapping components from npm in React Studio

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 anatomy of a plugin

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.

Let’s get down to writing code

Whether using just Main.js, or separating the code into multiple files, there are 5 main sections in a React Studio plugin:


  1. Plugin description
  2. Inspector UI
  3. Preview
  4. Custom interactions
  5. Code generation


For our example, we will put everything into Main.js.

Plugin description

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";

Inspector UI

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);
  }
}


Preview

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.

Custom Interactions

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.

Code generation

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.

Using your plugin

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.