Read return value from Ruby's add_action_callback in Javascript always undefined

Hi,

I face problem when I try to get return value from an action callback and the value that is read by Javascript is always undefined. I already read some posts that discuss the same issues like here and here and also read docs about add_action_callback but unfortunately I failed to implement it to my code.

Here is my code in Ruby side:

# add callback to calculate QTO
@dlg.add_action_callback("calculate") { |action_context|
     self.export_qto()
}

def self.export_qto()
     # grab active model
     model = Sketchup.active_model
     # initiate selected entities total volume
     message = "Volume of all selected entities: \n"
     vol = 0.0
     # select entities
     selected = model.selection
     if (selected.length > 0)
           selected.each { |item|
               if (item.typename == 'Group')
                   if (item.volume != -1)
                       vol = vol + item.volume
                       message = message + item.definition.name + '(' + item.typename + ')' + ': ' + format_volume_in_model_units(item.volume, 2) + "\n"
                   end
               end
           }
           puts message
           message
      else
           UI.messagebox('No entities selected. Select entities and try again!', type=MB_OK)
      end
end

And here is my code in Javascript side:

function calculate() {
       let hasil = []
       let result = sketchup.calculate({
           onCompleted: function (data) {
               console.log(data)
               hasil.push(data)
           }
       })
       if (result) {
           document.getElementById('hasilhitungan').innerHTML = hasil[0];
           console.log('Hasil: ', hasil[0]);
       }
}

However the resulting value is always undefined.

Do I have to always use execute_script on the Ruby side to be able to send the return value back to Javascript? Or I could still use this approach instead?

Thanks… really appreciate your helps…

Do you really need an html dialog? You can use a multiline message box.

You receive a string, why don’t you:

function calculate() {
    let hasil = ""
    sketchup.calculate({
        onCompleted: function (data) {
            console.log(data)
            hasil = data
        }
    })
    if (result) {
        document.getElementById('hasilhitungan').innerHTML = hasil;
        console.log('Hasil: ', hasil);
    } else {
        document.getElementById('hasilhitungan').innerHTML = "no result";
    }
}

Some suggestions…
You don’t need () inside if method

if item.is_a? Sketchup::Group && item.volume != -1

instead

if (item.typename == 'Group')
  if (item.volume != -1)

True. I was also going to say this.

But please use parenthesis around arguments for instance method calls. Ie …

if item.is_a?(Sketchup::Group) && item.volume != -1

@febrifahmi Please use 2 space indentation. This is what Ruby uses. Your 6 space indents cause us to need to scroll horizontally to read your code.

The above is incorrect. UI::messagebox does not accept named parameters. This is correct:

UI.messagebox('No entities selected. Select entities and try again!', MB_OK)

This statement is ridiculously much too long. Instead, in this case, you can use String#<< and double quoted String interpolation.

message << "#{item.definition.name} (#{item.typename}): #{format_volume_in_model_units(item.volume, 2)}\n"
1 Like

… because …

In the above snippet, result will be undefined because the sketchup.calculate call will return immediately. Your test if in … if (result) will likely always be false.

in other words, you’ve written the calculate function as if it’s statements will be synchronous and linear. They will not. The callbacks upon the JS sketchup object are asynchronous. So, result will always be undefined.

Ie, the call to sketchup.calculate() will return immediately with no value.

Also, it is quite likely that by the time the onCompleted JS callback gets called, that the JS function calculate will have returned and the hasil array will have gone out of scope and would have been garbage collected.
You may need to define the hasil array globally outside the calculate function.

Thanks for your reply and suggestion… Yes, I plan to improve the code so that I can send more detailed data (may be in the form of JSON) from Ruby to the HtmlDialog, not just using multiline message box.

Thanks for your detailed suggestion… really appreciate it…

I tried to change the function in the Javascript side so that the process of updating html is done inside the onCompleted: like this:

function calculate() {
  sketchup.calculate({
    onCompleted: function (data) {
      console.log(data)
      if(data !== null && data !== undefined){
        document.getElementById('hasilhitungan').innerHTML = `<p>${data}</p>`;
        console.log('Hasil: ', data);
      } else {
        document.getElementById('hasilhitungan').innerHTML = `<p>Data is ${data}</p>`;
        console.log(`Data is ${data}`)
      }
    }
  })
}

however, now I get null instead of undefined.

Thanks…

The good thing is that this proves that the Ruby side is called, and its action callback completes.

So, we need to look at what your Ruby dialog action callback block is returning. It simply calls your export_qto method.


Try not to use item.typename as it’s slow. Instead test for class identity or duck type.
ie :

      if (item.typename == 'Group')

… is better done using duck typing (… does it “quack” like we want it to?):

      next unless item.respond_to?(:volume)

This also saves an indent (within a loop) and no matching end is needed.


Then add a test for manifoldness …

      next unless item.respond_to?(:volume) && item.volume > 0

UI.messagebox returns an Integer. Probably should just return the message.


Side-Note: You are calculating cumulative volume but doing nothing with it.


Try …

def self.export_qto()
  # grab active model
  model = Sketchup.active_model
  # initiate selected entities total volume
  message = ""
  vol = 0.0
  # select entities
  selected = model.selection
  unless selected.empty?
    message = "Volume of all selected entities: \n"
    selected.each { |item|
      next unless item.respond_to?(:volume) && item.volume > 0
      vol = vol + item.volume
      message << "#{item.definition.name} (#{item.typename}): "<<
      "#{format_volume_in_model_units(item.volume, 2)}\n"
    }
    puts message
  else
    message = 'No entities selected. Select entities and try again!'
    puts message
    #UI.messagebox(message, MB_OK)
  end
  return message
end

Thanks for the correction in the way of writing the conditional and the loop…

But unfortunately I still get a null in the HtmlDialog. I tried to use the execute_script in the add_action_callback block to send the data to HtmlDialog but it still failed.

Now my code is like this in Ruby side:

@dlg.add_action_callback("calculate") { |action_context|
  message = self.export_qto()
  @dlg.execute_script("document.getElementById('hasilhitungan').innerHTML = '<p>Data is #{message}</p>'")
  message
}

def self.export_qto()
  # grab active model
  model = Sketchup.active_model
  # initiate selected entities total volume
  message = ""
  vol = 0.0
  # select entities
  selected = model.selection
  unless selected.empty?
    message = "Volume of all selected entities: \n"
    selected.each { |item|
      next unless item.respond_to?(:volume) && item.volume > 0
      vol = vol + item.volume
      message << "#{item.definition.name} (#{item.typename}): "<<
      "#{format_volume_in_model_units(item.volume, 2)}\n"
    }
    puts message
  else
    message = 'No entities selected. Select entities and try again!'
    puts message
    #UI.messagebox(message, MB_OK)
  end
  return message
end

and like this in JS side:

function calculate() {
  sketchup.calculate({
    onCompleted: function (data) {
      console.log(data)
      if(data !== null && data !== undefined){
        document.getElementById('hasilhitungan').innerHTML = `<p>${data}</p>`;
        console.log('Hasil: ', data);
      } else {
        document.getElementById('hasilhitungan').innerHTML = `<p>Data is ${data}</p>`;
        console.log(`Data is ${data}`)
      }
    }
  })
}

still don’t know where my mistake is in the code…

Something is missing at the end of this…

oh thanks, I guess some code is missing when I copied it.

You are correct. Apparently there are bugs in CEF v52 which came with SketchUp 2017.

I am testing to find out what the problem is.

The execute_script method is quirky. And I see problems with the command as you have it on multiple versions up through v2023.

The onCompleted JS callbacks work for SketchUp 2018 and higher.

Also, I have noticed that the DOM functions innerHTML and innerText can be quirky depending upon the Chromium version.

Okay I tested all day long.

Common Failures

  • Embedded quote characters both single (feet) and double (inches) in volume strings.
    This is the worst culprit in causing errors.

onCompleted JS Callback

SketchUp 2017 cannot return data to a JS callback. It’s either a bug or a limitation in CEF 52.

It does work on 2018 (CEF 56) and higher.

  • onCompleted works with embedded "\n" and innerText = is used:

execute_script

Using dialog.execute_script will work on 2017 and higher.

But, … there are strings that cause failures and errors in the JS.

Fails:

  • In 2017 and 2018 having embedded "\n" and using innerText = fails.

Works:

  • For 2017 and higher, execute_script works when "\n" is replaced with "<br>" and innerHTML = is used:

I did not have your format_volume_in_model_units method so I cobbled a temporary method.

I also played with sending the data over line by line which also works.

FabriFahmi_VolumeCalculator_v2.zip (2.6 KB)

1 Like

Thank you @DanRathbun … I don’t aware of that bug until you tested it… really appreciate your help! Thanks again…