Include functions in a plugin out of the constructor

It is probably to be a silly question but I am playing and learning these days about how to create a plug-in out of an npm package and I have a question. In most of the plug-in that I am trying to implement I have a function with bind that goes inside the constructor. No problem with that, I just include the line like the next one inside this.getReactWebRenderMethodSetupCode:

this.handleChange = this.handleChange.bind(this);

After this in the example that I am using, the function handleChange is called out of the constructor. Like so:

handleChange(value){
this.setState({text: value});
}

However if I include that in the this.getReactWebRenderMethodSetupCode I get an error, so surely I am not doing it fine.

How would I include that?

Hi, I don’t quite understand the problem yet. Could you send the relevant code pieces to us please? I.e. this.getReactWebRenderMethodSetupCode implementation in the plugin etc.

Of course! Thank you very much for your answer @juha_neonto. Let’s see if I explain better this time. This is the example that I am trying to replicate in the plugin. I have found more examples and I have the same with all of them.

Codepen example

In the example you can found the “bind” function inside the “constructor” but of it the function is called again with the “handleChange (html)”:

Captura de pantalla 2022-01-07 a las 12.13.41

Then in the render section if appears:
Captura de pantalla 2022-01-07 a las 12.14.36

So what I have done is:

this.getReactWebRenderMethodSetupCode = function(exporter, elementName) {

this.state = { editorHtml: ‘’, theme: ‘snow’ }
this.handleChange = this.handleChange.bind(this)

handleChange (html) {
this.setState({ editorHtml: html });
}

}

this.getReactWebJSXCode = function(exporter) {

var jsx =
<ReactQuill theme="snow" modules={this.modules} formats={this.formats} onChange={this.handleChange} value={this.state.editorHtml} />;
return jsx;
}

But I know the "handleChange (html) is not placed properly. I believe that as it is continuosly change of state after an interaction with the user (writting text), it should be done some other way in the plugin?

So I keep reading the documentation to understand how to properly write in the plugin the changes after the interaction, and also the part about loading content from a dataSheet and/or a dataSlot and saving the content in the dataSlot, which would be precisely the “this.state.editorHtml”.

Thank you so much!

We don’t currently support exactly that kind of implementation. Maybe you could try something like this:

this.getReactWebRenderMethodSetupCode = function(exporter, elementName) {
  this.state = { editorHtml: ‘’, theme: ‘snow’ }
  let handleChange = (html) => {
    this.setState({ editorHtml: html });
  }
}

this.getReactWebJSXCode = function(exporter) {
  var jsx =
  `<ReactQuill theme="snow" modules={this.modules} formats={this.formats} onChange={handleChange} value={this.state.editorHtml} />`;
  return jsx;
}

Hi Pedro,

I got REACT-QUILL to work as plugin.
You’ll need to create a new plugin like this:

Don’t use npm wrapper!

Then in Main.js put this code:
/*
React Studio plugin starter template for REACT-QUILL editor

Feel free to use and customise ... 

07/01/2022   Oliver Burrell

*/

// – plugin info requested by host app –

this.describePlugin = function(id, lang) {
switch (id) {
case ‘displayName’:
return “Editor”;

case ‘shortDisplayText’:
return “Editor”;

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

// – private variables –

// Here we store internally the values that the user can manipulate in the inspector UI, specified below.
this._data = {

};

// – inspector UI –

this.inspectorUIDefinition = [

];

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(“camera_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(“Editor”, w*0.5, h - fontSize/3);
}

ctx.restore();
}

// – 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-quill": "^1.3.5" };

}

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.

// if you need to set dataslots, ... different in each instance you'll need to comment the fixed className below out !!!
// return "AutocompletePicker";    // one className for all instances is used in this plugin

}

// not a ready made component!
this.writesCustomReactWebComponent = true;
// this.pluginAllowsMultipleInstances = true; // don’t think this does anything we need?

// Data links
this.reactWebDataLinkKeys = [

];

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

var view = {
    "CLASSNAME": className
};
var code = this.Mustache.render(template, view);

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

}

// generate calling props for component
this.getReactWebCustomJSXAttrs = function (exporter) {

}

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

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

}
return null;

}

this.getReactWebCSS = function(exporter, baseSelector, screenFormatId) {

}

Then in templates-web/component-template.js use this code:
import React, { Component } from ‘react’;
import ReactDOM from ‘react-dom’;
import ReactQuill from ‘react-quill’;
import PropTypes from ‘prop-types’;
import ‘react-quill/dist/quill.snow.css’;
import ScreenContext from ‘./ScreenContext’;

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

constructor (props) {
super(props)
this.state = { editorHtml: ‘’, theme: ‘snow’ }
this.handleChange = this.handleChange.bind(this)
}

handleChange (html) {
this.setState({ editorHtml: html });
}

handleThemeChange (newTheme) {
if (newTheme === “core”) newTheme = null;
this.setState({ theme: newTheme })
}

render () {
return (


<ReactQuill
theme={this.state.theme}
onChange={this.handleChange}
value={this.state.editorHtml}
modules={Editor.modules}
formats={Editor.formats}
bounds={’.app’}
placeholder={this.props.placeholder}
/>

Theme
<select onChange={(e) =>
this.handleThemeChange(e.target.value)}>
Snow
Bubble
Core



)
}
}

/*

  • Quill modules to attach to editor
  • See https://quilljs.com/docs/modules/ for complete options
    /
    Editor.modules = {
    toolbar: [
    [{ ‘header’: ‘1’}, {‘header’: ‘2’}, { ‘font’: [] }],
    [{size: []}],
    [‘bold’, ‘italic’, ‘underline’, ‘strike’, ‘blockquote’],
    [{‘list’: ‘ordered’}, {‘list’: ‘bullet’},
    {‘indent’: ‘-1’}, {‘indent’: ‘+1’}],
    [‘link’, ‘image’, ‘video’],
    [‘clean’]
    ],
    clipboard: {
    // toggle to add extra line breaks when pasting HTML:
    matchVisual: false,
    }
    }
    /
  • Quill editor formats
  • See https://quilljs.com/docs/formats/
    */
    Editor.formats = [
    ‘header’, ‘font’, ‘size’,
    ‘bold’, ‘italic’, ‘underline’, ‘strike’, ‘blockquote’,
    ‘list’, ‘bullet’, ‘indent’,
    ‘link’, ‘image’, ‘video’
    ]

/*

  • PropType validation
    */
    Editor.propTypes = {
    placeholder: PropTypes.string,
    }

/*

  • Render component on page
    */
    // ReactDOM.render(
    // <Editor placeholder={‘Write something…’}/>,
    // document.querySelector(’.app’)
    // )

Then create a test app in RS and drop the editor onto it.
It only works in the snow theme for now but this hopefully will give you a starting point …

If you want the html of the handleChange(html) moved to a dataSlot then it just needs one more line in handleChange:

and this in Main.js:

You’ll also need to save the data on the Interact tab like this:

Thank you so much both @juha_neonto and @Oliver_Burrell! I will try the suggestion by Juha later. I am right now trying the suggestion by Oliver and I am getting an error that is probably caused by me not following the guide fine:

Captura de pantalla 2022-01-07 a las 18.47.48

This is my main.js file:

// -- plugin info requested by host app --

this.describePlugin = function(id, lang) {
  switch (id) {
  case 'displayName':
    return "react-Quill";

  case 'shortDisplayText':
    return "react-Quill";

  case 'defaultNameForNewInstance':
    return "reactQuill";
  }
}


// -- private variables --

// Here we store internally the values that the user can manipulate in the inspector UI, specified below.
this._data = {
  
};

// -- inspector UI --

this.inspectorUIDefinition = [
  
];

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("camera_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("Camera", w*0.5, h - fontSize/3);
  }

  ctx.restore();
}

// -- 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 {"react-quill": "^2.0.0-beta.4"};
}

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

this.writesCustomReactWebComponent = true;

// Data links
this.reactWebDataLinkKeys = [

];

this.exportAsReactWebComponent = function(className, exporter) {
  var template = Plugin.readResourceFile("templates-web/component-template.js", 'utf8');
  
    var view = {
    "CLASSNAME": className
    };
    var code = this.Mustache.render(template, view);

    exporter.writeSourceCode(className + ".js", code);
}

// generate calling props for component
this.getReactWebCustomJSXAttrs = function (exporter) {

}

// WebInteractEvent
// IDs for the default interaction for plugins in React Studio
this.reactWebInteractEventsForStudioUI = [
	"valueChange"
	];

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

}
return null;
}

this.getReactWebCSS = function(exporter, baseSelector, screenFormatId) {

}

And this is my component-template:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import ScreenContext from './ScreenContext';

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

  constructor(props) {
    super(props);
    
    this.modules = {
    toolbar: [
        [{ 'font': [] }],
        [{ 'size': ['small', false, 'large', 'huge'] }],
        ['bold', 'italic', 'underline'],
        [{'list': 'ordered'}, {'list': 'bullet'}],
        [{ 'align': [] }],
        [{ 'color': [] }, { 'background': [] }],
        ['clean']
      ]
    };

    this.formats = [
      'font',
      'size',
      'bold', 'italic', 'underline',
      'list', 'bullet',
      'align',
      'color', 'background'
    ];
    
    this.state = { editorHtml: ‘’, theme: ‘snow’ }
	this.handleChange = this.handleChange.bind(this)
  }
  
  handleChange (html) {
	this.setState({ editorHtml: html });
	}
  
  render() {
    var jsx = 
      `<ReactQuill
      theme="snow"  
      modules={this.modules}
      formats={this.formats} 
      onChange={this.handleChange}
      value={this.state.editorHtml}
      />`;  
  return jsx;
  }
}

What did I do wrong?

Hi Pedro,

You’ll need to call it a different name because ReactQuill is the name of the imported component. You could call it just “Editor” or “HtmlEditor”.

You are right! I was missing the " return ‘Editor’ " in the this.getReactWebComponentName. Sorry about that. I just got the editor Working in the Screen! Thank you so much! You have no idea how much I appreciate it. Now I have to go but I will try to implement the part saving in the dataslot. And I will probably have to ask about loading previous content from a dataSheet. But let’s go step by step. Next thing to achieve is the dataSlot and your instructions are pretty clear.

Thank you so much!

I just implemented the dataSlot saving and it works perfectly fine. Having that I can easily save the content in the dataSheet through the dataSlot path. Thank you!

When I place the quillEditor in a Screen like an element in Scroll Flow, the content is supposed to push the underneath elements, however it does not:

The text field remains in its original position and the quill content overflows over it.

I believe the ideal situation would be to place the quillEditor in a component filling the entire component. Then I would drag the component to the screen but when I have tried that I receive an error:

I have commented that line helping to save the content in the dataSlot just to test the quillEditor in the component, and now it does not overflow over the elements below it, but I am not able to scroll the content when it is bigger than the size of the component.

Thank you so much!

I think you will have to reload the plugins from the plugins menu and then it will work.

Ok. I have achieved the CSS issue. I am not sure if I did it as I should. Firstly, I wanted to include the CSS values in the plugin itself so it is exported and I don’t need to manually change anything after exporting the project. I used the template of the values in the “TrendGraph” plugin. So what I did was to include in the “Main.js” in the plugin:

this.getReactWebCSS = function(exporter, baseSelector, screenFormatId) {
  var css = "";

  css += ""
  + `  width: 100%;\n`
  + `  height: 100%;\n`
  + ` background-color: while;\n`

  return css;
}

The background-color and the height are the two options that are achieving what I need. However, after exporting the project, if I open the App.css and goes to the section I can see this:

Captura de pantalla 2022-01-08 a las 1.11.08

The exported values are not included in any category. Probably it is something that I didn’t write fine. So I manually edited the CSS and added the background-color: white to the Component1, and changed the value for height to 100 % in the .Component1>.layoutFlow>.elReactQuill>*.

Captura de pantalla 2022-01-08 a las 1.16.40

That is just perfect for me. If I can get this editing the plugin, it would be great. If not, I will just manually change it after exporting.

One last thing that I mentioned before. Right now I can write, save the content to a dataSlot and then save it into a dataSheet (Firestore). But how would I load the content from many rows from a dataSheet?

I have a dataSheet that is a collection of posts. I would like to have the QuillEditor content to load this collection.

I am going to re-read again the documentation. I have been staring at examples code and reading the documentation and guide from Adam for a week and I have achieved things, but I need a more detailed explanation about some things that a little bit complex for my knowledge. Sorry about that :slight_smile:

Thanks!

This is the CSS I use in Main.js. It looks just like the Codepen example and uses the height of the RS component:

this.getReactWebCSS = function(exporter, baseSelector, screenFormatId) {
return ‘’+`
${baseSelector} {
margin: 1rem 4rem;
}

${baseSelector} .ql-container {
border-bottom-left-radius: 0.5em;
border-bottom-right-radius: 0.5em;
background: #fefcfc;
}

/* Snow Theme */
${baseSelector} .ql-snow.ql-toolbar {
display: block;
background: #eaecec;
border-top-left-radius: 0.5em;
border-top-right-radius: 0.5em;
}

/* Bubble Theme */
${baseSelector} .ql-bubble .ql-editor {
border: 1px solid #ccc;
border-radius: 0.5em;
}

${baseSelector} .ql-editor {
min-height: 18em;
height: 100%;
}

.themeSwitcher {
margin-top: 0.5em;
font-size: small;
}`
}

Regarding your further design question … I don’t quite understand what you are trying to do. Maybe you could post this into a new thread and show your data structure and explain in more detail what you want to achieve?

Thank you @Oliver_Burrell! I created a new thread explaining the thing achieve. I hope I explained it well.