How to separate the plugin code/class if used more than once with different parameters?

I wrote an Autosuggestion Combobox Plugin and am trying to use it twice on the same screen.
It works fine if I use it only once on a screen but not if used twice.
What kind of plugin code do I need to write to separate the classes/code … ?

Here is the plugin code:
Main.js
this.describePlugin = function(id, lang) {
switch (id) {
case ‘displayName’:
return “Autosuggest Picker”;

case ‘shortDisplayText’:
return “Autosuggest Picker”;

case ‘defaultNameForNewInstance’:
return “AutosuggestPicker”;
}
}

// – private variables –

// Here we store internally the values that the user can manipulate in the inspector UI, specified below.
this._data = {
sourceDataSheet: “”,
suggestedDataColumn: “”,
selectedColumn: “”,
searchDataSlot: “”,
placeholderText: “type to see suggestions”
};

// – inspector UI –

this.inspectorUIDefinition = [
{
“type”: “label”,
“text”: “A link to the suggestions data sheet:”
}, {
“type”: “datasheet-picker”,
“id”: “sourceDataSheet”,
“actionBinding”: “this._onUIChange”
},
{
“type”: “label”,
“text”: “Name of the field with the suggestion data:”
}, {
“type”: “textinput”,
“id”: “suggestedDataColumn”,
“actionBinding”: “this._onUIChange”
},
{
“type”: “label”,
“text”: “Values attached to picker’s hidden value field. Usually this contains the ID field:”
},
{
“type”: “textinput”,
“id”: “selectedColumn”,
“actionBinding”: “this._onUIChange”
},
{
“type”: “label”,
“text”: “A link to a data slot:”
}, {
“type”: “dataslot-picker”,
“id”: “searchDataSlot”,
“actionBinding”: “this._onUIChange”
},
{
“type”: “label”,
“text”: “Placeholder text:”
},
{
“type”: “textinput”,
“id”: “placeholderText”,
“actionBinding”: “this._onUIChange”
},
];

this._accessorForDataKey = function(key) {
// This method provides a convenient way to map controls declared in “inspectorUIDefinition” to properties specific for each control type.
// Both onCreateUI and onUIChange will call this method.
// It is assumed here that your _data object will store values with the same property names as each controlId.
// You don’t have to use this for your own UIs, but it’s a convenient way to get two-way binding between the UI and your _data object.
//
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 private _data 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 is the “actionBinding” specified for controls where we want to use the automatic binding to _data.
var ui = this.getUI();
var prop = this._accessorForDataKey(controlId);
if (prop) {
this._data[controlId] = ui.getChildById(controlId)[prop];
} else {
console.log("** no data property found for controlId "+controlId);
}
}

// – persistence, i.e. saving and loading –

this.persist = function() {
// The object returned here must be serializable to JSON.
// Our private _data object fills this requirement fine.
return this._data;
}

this.unpersist = function(data) {
// If your plugin has multiple versions and you’ve added properties to your _data object,
// you may want to do a check here to see if the incoming “data” was written using an old version of the plugin
// and fill in any missing properties.
this._data = data;
}

// – plugin preview –

this.renderIcon = function(canvas) {
this._renderPreview(canvas, false);
};

this.renderEditingCanvasPreview = function(canvas, controller) {
this._renderPreview(canvas, true, controller);
}

this._renderPreview = function(canvas, showText, controller) {
var ctx = canvas.getContext(‘2d’);
var w = canvas.width;
var h = canvas.height;
ctx.save();

// When drawing to an editing canvas, the output context may be a high-DPI (“Retina”) display.
// We’d like to draw in familiar “web pixels”, so we need to find out the display’s render scale factor.
// Currently on Mac, this scale factor will be 2 if it’s a Retina display, 1 otherwise.
var displayScale = 1;
if (controller && (displayScale = controller.renderPixelsPerWebPixel)) {
ctx.scale(displayScale, displayScale);
}

if (showText) { // fill background with color
var color = this._data.exampleColorValue;
if (color && color.length >= 4) {
ctx.fillStyle = rgba(${255*color[0]}, ${255*color[1]}, ${255*color[2]}, ${color[3]});
} else {
ctx.fillStyle = “rgba(0, 0, 0, 0.3)”;
}
ctx.fillRect(0, 0, w, h);
}

if (this.icon == null) {
var path = Plugin.getPathForResource(“autosuggest_icon.png”);
this.icon = Plugin.loadImage(path);
}
if (this.icon) {
var iconW = this.icon.width;
var iconH = this.icon.height;
var aspectScale = Math.min(w/iconW, h/iconH);
var scale = (showText ? 0.7 : 0.6) * aspectScale; // add some margin around icon
iconW *= scale;
iconH *= scale;
ctx.save();
ctx.globalAlpha = (showText) ? 0.8 : 0.5;
ctx.drawImage(this.icon, (w-iconW)/2, (h-iconH)/2, iconW, iconH);
ctx.restore();
}

if (showText) { // render a label at the bottom
var fontSize = (h > 40) ? 30 : 15;
ctx.fillStyle = “#ffffff”;
ctx.font = fontSize+“px Helvetica”;
ctx.textAlign = “center”;
ctx.fillText(“Autosuggest Picker”, w*0.5, h - fontSize/3);
}

ctx.restore();
}

// – custom interactions –

// This JSON array declares the actions we want to make available in React Studio.
// The actual code to handle each action will be generated by the method
// ‘writeReactWebCodeForPublishedInteractAction’ found below.
this.publishedInteractActions = [

];

// – code generation, platform-independent –

this.getLinkedElements = function () {
// Using this entry point, we tell the host app that we need references to other elements within the same screen / block.
// For each element that we’re linking to, we provide “elementId” and “propertyName” that should contain the reference.
// This way the Design Compiler on the host app will know to write those values into the generated classes.
// When we’re generating code, we ask the Design Compiler to provide code for these properties using “exporter.getPropertyDeclsForLinkedElements()”
return [
// {
// “elementId”: this._data.linkedElementId,
// “propertyName”: “linkedElement”,
// }
];
}

// – code generation, React web –

this.getReactWebPackages = function () {
// Return dependencies that need to be included in the exported project’s package.json file.
// Each key is an npm package name that must be imported, and the value is the package version.
//
// Example:
// return { “somepackage”: “^1.2.3” }

return { "react-autosuggest": "^10.1.0" };

}

this.getReactWebComponentName = function () {
// Preferred class name for this component.
// The exporter may still need to modify this (e.g. if there already is a component by this name),
// so in the actual export method below, we must use the className value provided as a parameter.

return "AutosuggestPicker";

}

// not a ready made component!
this.writesCustomReactWebComponent = true;

// Data links
this.reactWebDataLinkKeys = [
// “initialPlaceText”, “initialGeoLocation”, “country”, “radius”
“value”
];

this.exportAsReactWebComponent = function (className, exporter) {
var template = Plugin.readResourceFile(“templates-web/component-template.js”, ‘utf8’);

var elementName = this.getElementName();
elementName = elementName.split(' ').join('');  // remove blanks

var dataSheetCode = exporter.valueAccessForDataSheetByName(this._data.sourceDataSheet);
// console.trace('code: ' + dataSheetCode);

var searchDataSlotCode = null;
if (this._data.searchDataSlot && this._data.searchDataSlot.length > 0) {
  searchDataSlotCode = exporter.setValueCodeForDataSlotByName(this._data.searchDataSlot, "");
}

var view = {
    "CLASSNAME": elementName,
    "SOURCEDATASHEET": dataSheetCode,
    "SUGGESTEDDATACOLUMN": this._data.suggestedDataColumn,
    "SELECTEDCOLUMN": this._data.selectedColumn,
    "SEARCHDATASLOT": searchDataSlotCode,
    "PLACEHOLDERTEXT": this._data.placeholderText
};
var code = this.Mustache.render(template, view);

exporter.writeSourceCode(elementName + ".js", code);

}

// generate calling props for component
this.getReactWebCustomJSXAttrs = function (exporter) {
var propsStr = ‘’;

const dataSheetItemsProp = exporter.valueAccessForDataSheetByName(this._data.sourceDataSheet);
if (dataSheetItemsProp) propsStr = propsStr +  "suggestions={" + dataSheetItemsProp + ".items}";

const valueChangeCode = exporter.getCallbackForInteractEvent('valueChange');
if (valueChangeCode) propsStr = propsStr +  " onChange={"+valueChangeCode+"}";

return propsStr;

}

// WebInteractEvent
this.reactWebInteractEventsForStudioUI = [
“valueChange” // id for the default interaction for plugins in React Studio
];

this.describeReactWebInteractEvent = function(exporter, interactionId) {
switch (interactionId) {
case ‘valueChange’:
return {
actionMethod: {
arguments: [‘newValue’],
getDataExpression: ‘newValue’
}
};
}
return null;
}

this.writeReactWebCodeForPublishedInteractAction = function (actionId, exporter, varName, arg) {

}

this.getReactWebCSS = function(exporter, baseSelector, screenFormatId) {
return ‘’+`
${baseSelector} .react-autosuggest__container {
position: relative;
}

${baseSelector} .react-autosuggest__input {
font-family: ‘Montserrat-Regular’, sans-serif;
font-size: 18.0px;
box-sizing: border-box;
display: block;
background-color: transparent;
color: rgba(0,0,0,.745);
border: none;
border-bottom: 1px solid rgba(0,0,0,.26);
outline: 0;
width: 100%;
padding: 0;
box-shadow: none;
border-radius: 0;
line-height: inherit;
pointerEvents: ‘auto’;
}

${baseSelector} .react-autosuggest__input–focused {
border-color: #2196F3;
border-width: 2px;
}

${baseSelector} .react-autosuggest__input–open {
}

${baseSelector} .react-autosuggest__suggestions-container {
display: none;
}

${baseSelector} .react-autosuggest__suggestions-container–open {
display: block;
position: absolute;
top: 23px;
width: 100%;
border-top: 2px solid #2196F3;
border-right: 1px solid #aaa;
border-bottom: 1px solid #aaa;
border-left: 1px solid #aaa;
background-color: #fff;
z-index: 2;
}

${baseSelector} .react-autosuggest__suggestions-list {
margin: 0;
padding: 0;
list-style-type: none;
}

${baseSelector} .react-autosuggest__suggestion {
cursor: pointer;
padding: 10px 20px;
}

${baseSelector} .react-autosuggest__suggestion–highlighted {
background-color: #ddd;
}`
}

component-template.js

import React, { Component } from ‘react’;
import Autosuggest from “react-autosuggest”;
import ScreenContext from ‘./ScreenContext’;

// https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[]\]/g, ‘\$&’);
}

export default class {{{CLASSNAME}}} extends Component {

static contextType = ScreenContext;

constructor(props) {
super(props);

this.state = {
  value: this.props.value,
  renderSuggestions: false
};

}

onChange = (event, { newValue }) => {
this.setState({
value: newValue,
renderSuggestions: true
});
};

getSuggestionValue = (suggestion) => {
return suggestion[’{{{SUGGESTEDDATACOLUMN}}}’];
}

renderSuggestion = (suggestion) => {
return (
{suggestion[’{{{SUGGESTEDDATACOLUMN}}}’]}
);
}

updateSearchDataSlot = (value) => {
const escapedValue = escapeRegexCharacters(value.trim());

let saveTosearchDataSlot = null;
{{#SEARCHDATASLOT }}
saveTosearchDataSlot = {{{SEARCHDATASLOT}}};
{{/SEARCHDATASLOT}}
if (saveTosearchDataSlot && escapedValue) {
  saveTosearchDataSlot(escapedValue);
}

}

onSuggestionsFetchRequested = ({ value, reason }) => {
if (reason === ‘input-changed’) {
// load datasheet here by updating the ds_input
this.updateSearchDataSlot(value);
}
};

// function onSuggestionSelected(event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method })
onSuggestionSelected = (event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) => {
// selected, move selected data to output
this.updateSearchDataSlot(suggestionValue);
this.props.onChange((suggestion.id).toString());
this.setState(prevState =>{
return{
…prevState,
renderSuggestions: false
}
})
}

render() {
const inputProps= {
placeholder: “{{{PLACEHOLDERTEXT}}}”,
value: this.state.value,
onChange: this.onChange,
autoFocus: true
};

return (
  <div className="{{{CLASSNAME}}}">
    <Autosuggest
      suggestions={this.props.suggestions}
      onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
      getSuggestionValue={this.getSuggestionValue}
      renderSuggestion={this.renderSuggestion}
      inputProps={inputProps}
      onSuggestionSelected={this.onSuggestionSelected}
      highlightFirstSuggestion={true} />
  </div>
);

}
}

If you remove getReactWebComponentName function (the one that returns “AutosuggestPicker”) it generates separate classes based on the element names.

Hi Juha,
Many thanks, that works brilliantly :grin: