Get info from ruby action callback to javascript

I’m trying to get info from ruby script callback to a javascript function but i’m not able to receive the data.

I have this in my html file (I want a drop-down list):

<select class="despl-01" name="capa" id="id10"></select>

I have this in ruby(I nned the layer list):

dialog.add_action_callback("layers_to_form") {|action_context|
	json_text = Sketchup.active_model.layers.map(&:name).to_json
	layers = "#{json_text}"
}

Then inside javascript I have this function in order to send layer list to drop-down list in html file:

function listOfLayers () {
    sketchup.layers_to_form()
    var layersReceived = layers
    var arrayLength = layersReceived.length;
    var layerList = "<option value='0'>select layer</option>";
    for (var i = 0; i<arrayLength; i++) {
        layerList += "<option value='"+i+"'>"+layersReceived[i]+"</option>"
    }
    document.getElementById('id10').innerHTML = layerList
}

What am I doing wrong? I receive nothing in my drop-down list id10

How do you run the html-side’s function

listOfLayers 

The layers variable in your Ruby code has nothing to do with the (currently undeclared) variable layers in JavaScript.

You need to “inform” JavaScript e.g. with the #execute_script method about this variable within the action callback.
One of the possibility could be to declared variable outside a function, in your JavaScript and wait for the Ruby side to be completed, then you can build the list.

( The #add_action_callback method is asynchronous. JavaScript call might return before Ruby callback even called. Use onCompleted callback to get notified for completion.)

Without I checked my code below I assume you need to do something like this:

Ruby:

dialog.add_action_callback("layers_to_form") {|action_context|
  json_text = Sketchup.active_model.layers.map(&:name).to_json
  dialog.execute_script("layers = JSON.parse(#{json_text});")
}

JavaScript :

var layers = [];
function listOfLayers () {
  sketchup.layers_to_form({
    onCompleted: function() {
      var layersReceived = layers;
      var arrayLength = layersReceived.length;
      var layerList = "<option value='0'>select layer</option>";
      for (var i = 0; i<arrayLength; i++) {
          layerList += "<option value='"+i+"'>"+layersReceived[i]+"</option>";
      }
      document.getElementById('id10').innerHTML = layerList;
    }
  });
}

I run the html function with:

document.getElementById('id10').innerHTML = layerList;

It doesn’t works. The drop-down list only receive “select layer”
This part doesn’t attach nothing to layerList

for (var i = 0; i<arrayLength; i++) {
     layerList += "<option value='"+i+"'>"+layersReceived[i]+"</option>";
}


Rather than work with html in js, maybe use the DOM? I know it’s another API to learn, but it’s pretty well optimized…

EDIT:

I haven’t worked with plugins for a while, but I found the function used for loading select elements. One function call could load multiple select elements, using nested arrays passed to js from Ruby.

const setOptions = (a) => {
  a.forEach( ai => {
    let el = $(ai[0]),
        opts = ai[1];
    el.options.length = opts.length;
    opts.forEach( (txt,i) => el.options[i].text = txt );
  });
};

This would make two options with an index of 0.

Yes it’s true but I changed var = 0 by var = 1 and the problem remains…

Try the following:

Ruby

dialog.add_action_callback("layers_to_form") {|action_context, select_id|
  lyr_ary = Sketchup.active_model.layers.map(&:name).prepend 'select layer'
  es_para = [[select_id, lyr_ary]]
  es = "setOptions(#{es_para.inspect});"
  dialog.execute_script es
}

js

function listOfLayers () {
    sketchup.layers_to_form('id_10')
};

const setOptions = (a) => {
  a.forEach( ai => {
    let el = document.getElementById(ai[0]),
        opts = ai[1];
    el.options.length = opts.length;
    opts.forEach( (txt,i) => el.options[i].text = txt );
  });
};

Remove document.getElementById('id10').innerHTML = layerList;

1 Like

It seems to work but it fails reading “ai”

In my code:

32 const setOptions = (a) => {
33   //console.log(a);
34    a.forEach( ai => {
35       console.log(ai);
36    let el = document.getElementById(ai[0]),
37        opts = ai[1];
38        //console.log(el);
39       //console.log(opts);
40    el.options.length = opts.length;
41    opts.forEach( (txt,i) => el.options[i].text = txt );
42 });
43};

It returns main.js:40 Uncaught TypeError: Cannot read property ‘options’ of nul

I attach a screen capture if it helps

Given the error message, it seems that the below code is setting el to null. But, MDN’s docs of the ‘Options’ collection indicate that in some browsers ‘el.options.length’ may be readonly.

If that’s now the case with SU (I believe it worked with some versions), then change:

el.options.length = opts.length

to

optsLen = opts.length;
while (el.options.length > opts.length) { el.options.remove(el.options.length - 1) }

I.Think. I’m kind of busy with some Ruby stuff. You might also check that ‘el’ is an element…

I made a mistake. Before trying the above, I posted the following line of code:

it should be:

sketchup.layers_to_form('id10')

IOW, you listed the select element’s id as id10, but my ‘suggested’ code used id_10. Sorry.

1 Like

Thank you very much @MSP_Greg it works perfectly. That was the reason why the drop-down menu didn´t receive the data.
I owe you one!

The onCompleted callback on JS should be able to receive the return value from Ruby, so if you pull data from JS you don’t need to use execute_script to push the result back from Ruby:

Something like this (untested):

Ruby:

dialog.add_action_callback("layers_to_form") {|action_context|
  # This returns a JSON string from this callback.
  Sketchup.active_model.layers.map(&:name).to_json
}

JavaScript :

function listOfLayers () {
  sketchup.layers_to_form({
    onCompleted: function(json) {
      var layersReceived = JSON.parse(json); # Parse the JSON here
      var arrayLength = layersReceived.length;
      var layerList = "<option value='0'>select layer</option>";
      for (var i = 0; i<arrayLength; i++) {
          layerList += "<option value='"+i+"'>"+layersReceived[i]+"</option>";
      }
      document.getElementById('id10').innerHTML = layerList;
    }
  });
}

If you return only simple values, (not arrays or hashes etc) you shouldn’t even need to return a JSON string, SU should take care of string and numeric conversions for you. (Might not even be necessary to do to_json on that array in this example)

2 Likes

:+1: Good advice. I would never have found it myself. I’ll write it in my “lesson book”! Thanks!

I also wasn’t aware of that. You’re a little unclear as to what type of Ruby objects can be passed to the onCompleted function. Would certainly be helpful if ‘base’ Ruby objects could be parsed by the ‘Ruby to js’ transition. Might need some restrictions, ie, how symbols are handled, nesting level, etc…

But, as I recall with older versions of SU, Ruby’s inspect/to_s methods will generate a string that can be easily used in an execute_script string as a parameter, and hence, no need for json.

It worked too. I had only to remove the comment ruby #Parse the JSON here by js //Parse the JSON here but I think I should have notice before trying. I’m not an expert in JS.
Thanks to all of you.

To be honest, I just realized this myself a few days ago. The docs could do with some improvement in regard to explaining the JS side callbacks.

1 Like

I threw some code together, and with the select element’s simple child elements (<option>), the technique used to load the select element doesn’t really matter. I tried four cases:

OnCompletedInnerHTML
OnCompletedDOM
ExecuteScriptDOM
ExecuteScriptDocumentFragment

Even with 300 options elements, timing from the call from the dialog to load/reload the select, thru Ruby, to the completion of the html document changes, they typically took between 12 and 15 mS to load the select element, and there weren’t consistent times.

Timing just the changes to the document in the dialog code (js only), the times were between 1.5 and 2.6 mS to add the <option> elements to the select element.

I’ll post a zip sometime soon.

Some notes:

  1. If one is using innerHTML, one should consider HTML encoding the inner text. This can be done in Ruby with CGI.escapeHTML, but there isn’t a similar function in js. I believe when using DOM functions. all text is encoded as it’s assigned. Not.sure.

  2. OnCompleted js block/function - with SU 2019, I tried passing an array, and it didn’t work. Didn’t try a hash. So, it may need to be a json string.

  3. A while ago I had non-modal dialogs that mimicked SU’s native layer and material lists, as meta data could be attached to both. Don’t recall if they were ul/li lists or tables (tables I think). Hence, the child elements were much more complex. Given that, using a documentFragment was the best choice for fast rendering.