Load data from dataSheet/dataSlot (Firestore) in a plugin

I am still reading the documentation and studying examples from the camera, trendgraph and some other examples but still I need a last push to understand how to get it done. So here’s what I have.

I have a Firestore collection already configured in a dataSheet in RS. The dataSheet is “posts” and the specific field that I want to retrieve is “cuerpo”:

Then I have the react editor “Quill” in a plugin with a JSX template which is working great thanks to the wonderful help given by @Oliver_Burrell. Now I have a screen where I place a list/grid to load all the posts collection. So I have a component for each one of these posts where I would show the author, date, and the body of the post. I want that body to be shown by the plugin (quillEditor) so it is shown as rich text and not as raw html.

So I would need that the plugin would show the “body” field in the posts dataSheet. I believe the section to change things would be:
Captura de pantalla 2022-01-08 a las 11.40.14

Captura de pantalla 2022-01-08 a las 11.40.34

Thanks in advance!

Why do you want to show it in the Quill editor?
Do you want to edit the posts in that list?

No need to edit the posts in that list. But when I save the content in the editable quill plugin, the content is saved with html tags like so:

If I use a text field or any of the options in RS, the content will be shown with the tags instead of the formatted text. So I was creating two versions of the plugin: the snow theme with the toolbar in order to create or edit posts, and another one with the bubble theme being readOnly and without any toolbar, but showing the formatted text.

Maybe there is some other way to show the text just fine without needing to load the plugin and the dataSheet within the plugin?

Ah, you’ll need to display the text as HTML.
There is an “Embed Content” field in RS and you could use the Data tab text field to feed the HTML in.
See an example below:

I see! So no need to prepare a specific version of the plugin to show the text. I just tried the html embed content and it shows the text just fine. One issue though. It seems that the field is not aware of its content?

You can see in the screenshot that there is an icon below the html embed element in scroll flow. Supposedly it should push the underneath elements but it doesn’t:

Something I am doing wrong?

Sorry, what is not pushed down?

The icon below the html box. In the RS screenshot is placed below the box, but in the output the text from the html goes over it. You can see it in the first screenshot in the previous message.

The Embedded Content element is just showing the raw HTML. If you need it to have margins or other CSS around it then you’ll need to enclose it in a div /div and format the div with css. Best is to debug it in Chrome and enter different values in the css for the div element (e.g. the height). Then you’ll find out what’s missing …
You could also create a frame around it this way.

Another and maybe easier way is if you put the embedded content into a new component. Then you can create frames and it will push down.
I tried it out:

I believe my issue is because the html element is placed in a component. I tried to create an independent component with just the html and a background and then place it in the addPost component wich is the component used to fill out the list/grid, but it does not push the underneath content. Maybe because it is component?

So if I don’t find a simple solution I have two options:

  • I can manually edit the CSS after exporting the project finding the way to get it the way I want as I cannot achieve the CSS within RS. I don’t like this option too much because I would have to edit this manually for every export.
  • I would rearrange elements so the html is the last thing in the component. I would place the icons over it in some place, even though I like those icons there because they just appear if the user is the author of the post, and they contribute to the scroll flow, so I would have to play around with the configuration to have a good output if they are present or if they are not.

Thank you so much @Oliver_Burrell. I could fix the CSS problem and no need to manually change anything after exporting. I have been playing around with the CSS and the configuration of the elements and now everything is in order.

One thing I have realized after setting everything. I have no problem creating new posts with the quill editor, and no problem listing the collection in the main screen. However I cannot achieve to load the content from an existing post in the plugin editor. I thought that it would be easy if the content is loaded in a dataSlot, but it is not. I have tried to load the content from the dataSlot inside the “component_template.js” with:

var incipit = this.context.appActions.dataSlots[‘ds_comentarios’];

But it shows an error compiling. I am guessing that I have to do the linking through the main.js code in the plugin so I have gon there and add:

// – inspector UI –

this.inspectorUIDefinition = [
{
“type”: “label”,
“text”: “A link to a data slot:”
}, {
“type”: “dataslot-picker”,
“id”: “searchDataSlot”,
“actionBinding”: “this._onUIChange”
},
];

With that, I have the hability to select a dataSlot in the UI:

However the content is not loaded. I am sure I need to specify and point that but I don’t know how to make the reference.

Sorry about my clumsiness. I believe I am almost there.

Thanks!

Hi @petoma,

In Main.js you’ll need to feed the value from the dataSlot you used to store the html into into the component like this:

Define it earlier like this (you can use a different name for the dataSlot if you like but then you’ll need to change the code further down …):

Then in the component-template you just need to initialise the state with this data like this:

Then it should work …
Good luck!

Thank you so much @Oliver_Burrell! It did work and now it is possible to edit an already publised post. I really appreciate your help here. I need to keep learning because even though I can understand the logic in the code when I see it, I wouldn’t be able to write it by myself.

Thanks! :slight_smile:

Hi @petoma,

A trainings course in REACT would probably be good for you to learn more and fast. There is a free course on https://scrimba.com/learn/learnreact which is quite good.

Thanks for the resource @Oliver_Burrell! I’ll do that one. I have been some time learning just vanilla JS because I wanted to have good grounds before digging into REACT. I have some understanding of course of React because I have been reading and learning about both at the same time at some points. But my priority was JS so I could use the script feature to better personalize the project.

Now I feel more comfortable with JS. Still not able to do complex things, but confident enough to write some basic things. I’ll try to have some time to do that React course you suggested.

Thank you!

1 Like

I am sorry to bring this back again but I am implementing one detail related with the text editor plugin. Everything is working great but recently I have included the routing parameter option just to be able to send a url as a deep link to a specific post that has instructions for some task. It works fine except for the fact that the html content is loaded from a dataslot and that works just fine if you are navigating within the app, but it won’t work if you visit the URL directly from a provided link.

The solution is easy, instead of loading the content from a dataSlot, is should load it from a datasheet. I have tried if it works with some other text fields in the screen and it does. So I would need to change the plugin code to load the content instead of a dataSlot but from a field/value from a dataSheet.

I will dedicate some time to read the documentation this afternoon but it was quite difficult for me last time I approached it so I would really appreciate some guidance. Here I have the code from my plugin:

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

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

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

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


// -- private variables --

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

// -- inspector UI --

this.inspectorUIDefinition = [
    {
      "type": "label",
      "text": "A link to a data slot:"
      }, {
      "type": "dataslot-picker",
      "id": "ds_bubble",
      "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("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 "bubbleEditor";
}

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) {
	var valueChangeCode = exporter.getCallbackForInteractEvent('valueChange');
    var valueCode = exporter.valueAccessForDataSlotByName(this._data.ds_bubble);
  
  if (valueChangeCode){
   	var jsx = `onChange={${valueChangeCode}}`;
  }
  if (valueCode) {
    jsx += `value={${valueCode}}`; 
  }
  return jsx;
}

// 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) {
  return ''+`
    ${baseSelector} {
    margin: 1rem 4rem;
    }

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

    ${baseSelector} .ql-editor {
    min-height: 25em;
    height: 100%;
	margin-top: 0px;
	bakground-color: white;
    }`
}

And this is the component-template:

import React, { Component } from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.bubble.css';

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

  constructor(props) {
    super(props);
    
	this.state = {editorHtml: (this.props.value !== undefined) ? this.props.value: ''}
    this.handleChange = this.handleChange.bind(this)
  }
  
  handleChange (html) {
	this.setState({ editorHtml: html });
    this.props.onChange(html);
	}
  
  render() { 
  return (
    <ReactQuill
      theme="bubble"
      onChange={this.handleChange}
      value={this.state.editorHtml}
      readOnly={true}
      />
    );
  }
}

Thanks!