WebDialog / HtmlDialog Tutorial using sketchup-bridge

WebDialog / HtmlDialog Tutorial using sketchup-bridge

SketchUp has two classes for creating UI dialogs:

WebDialogs had several problems that are deeply covered in the Lost Manual. With HtmlDialog, developers still face two major difficulties that cause people to spend over and over again development time to build their own solutions instead of just building extensions:

  • There is not yet a direct foreign function invocation from Ruby to JavaScript (analog to JavaScript to Ruby: sketchup.callbackname()). While developers can use execute_script, they have to take care every single time about encoding parameters properly into a valid JavaScript string.

  • Continuous control flow is still broken into pieces because of asynchronicity. While it is possible to invoke a function and pass data from either side, it is not easy to communicate back and forth in a continuous manner (like synchronous code): JavaScript→Ruby→JavaScript→…
    sketchup.callbackname(...parameters, { 'onCompleted': function () {} }) allows to invoke a JavaScript function after the Ruby callback completed, but it neither transfers the Ruby return value nor does it give feedback about success/failure.

I want to propose a library that I have been using for several years and that I think has worked well enough for me that it can serve others as well. You don’t have to use it, but if you stumble across hurdles with working directly with HtmlDialog, think about it.

Idea

My idea was to use Promises. Promises help us to deal with asynchronous programming.

  • Compared to the callback function pattern (callback at the end within the parameters list like onCompleted), callbacks are attached onto the returned promise object, which avoids clashes in the parameters list.
  • Promises provide two feedback channels for success and failure. So the developer can decide whether to handle errors (or some errors) on the Ruby side or JavaScript side.
  • Promises work with modern JavaScript async and await.

Using WebDialogs/HtmlDialogs

If you wanted to implement JavaScript→Ruby→JavaScript you would probably have done something similar to this:

WebDialog
function myFunction1 () {
  // ...
  window.location = 'skp:compute_area@' + width + ',' + length;
  // You cannot get the computation result here and continue 
  // (like displaying it in the UI).
}
dialog.add_action_callback('compute_area') { |dlg, parameter_string|
  width, length = parameter_string.split(',').map(&:to_f)
  # If this fails, the error goes unnoticed.
  result = compute_area(width, length)
  # You could create invalid JavaScript if you 
  # concatenate strings "myFunction2(" + result.to_s + ")"
  dlg.execute_script("myFunction2(#{json.generate(result)})")
}
function myFunction2 (result) {
  // How to identify to which HTML element this result belongs to?
  // This is out of the context of myFunction1, 
  // we cannot access variables to HTMLElements.
  document.getElementById('myElement').value = result;
}

HtmlDialog

function myFunction1 () {
  // ...
  sketchup.compute_area(width, length);
  // You cannot get the computation result here and continue 
  // (like displaying it in the UI).
}
dialog.add_action_callback('compute_area') { |dlg, width, length|
  result = compute_area(width, length)
  # You could create invalid JavaScript if you 
  # concatenate strings "myFunction2(" + result.to_s + ")"
  dlg.execute_script("myFunction2(#{json.generate(result)})")
}
function myFunction2 (result) {
  // How to identify to which HTML element this result belongs to?
  // This is out of the context of myFunction1, 
  // we cannot access variables to HTMLElements.
  document.getElementById('myElement').value = result;
}

Using the sketchup-bridge library

Instead of making your Ruby code push data to the dialog, you make the dialog fetch data from Ruby whenever it needs. That means the Ruby part acts like a server, and the dialog is the client.

function myFunction1 () {
  // ...
  Bridge.get('compute_area', width, length).then(function (result) {
    // The callbacks have access to all variables in the scope of myFunction1.
    // This makes it practical for object-oriented programming, e.g. if you have
    // a whole table of elements to update.
    document.getElementById('myElement').value = result;
  }).catch(function (error) {
    alert('The computation failed: \n' + error);
  });
}
dialog.on('compute_area') { |deferred, width, length|
  result = compute_area(width, length)
  deferred.resolve(result)
}

Using modern JavaScript (HtmlDialog with Chromium 55+ / ECMAScript 2017) it gets simplified to:

function myFunction1 () {
  // ...
  let result = await Bridge.get('compute_area', width, length)
  document.getElementById('myElement').value = result
}

which gets rid of all the boilerplate and looks and reads like synchronous code.

References

6 Likes

I am trying to test this extension on SU 2015 on macos, but I keep receiving the error

NameError: undefined local variable or method `dialog' for #<AuthorName::SampleExtension::Bridge::RequestHandlerWebDialog:0x007f8cfffc8580>Error: #<TypeError: no implicit conversion of String into Integer>
/Users/ruggiero/Library/Application Support/SketchUp 2015/SketchUp/Plugins/sample_extension/bridge.rb:539:in `<<'
/Users/ruggiero/Library/Application Support/SketchUp 2015/SketchUp/Plugins/sample_extension/bridge.rb:539:in `log_error'
/Users/ruggiero/Library/Application Support/SketchUp 2015/SketchUp/Plugins/sample_extension/bridge.rb:659:in `rescue in receive'
/Users/ruggiero/Library/Application Support/SketchUp 2015/SketchUp/Plugins/sample_extension/bridge.rb:664:in `receive'
SketchUp:1:in `call'

I have tried to install the sample application with no modifications and also to embed it in my plugin, but there is no difference. Any suggestion?

I have downloaded the files from your repository on GitHub.

Thanks!

Probably the switch to ES6 and compiling down to ES5 does not really output JavaScript that is compatible with older system browser engines (Internet Explorer, Safari) in SketchUp < 2017.
You could git checkout be89411447 to get the previous version.

The difference I see between a server and seeing ruby, and thus sketchup, as a server, is that in this case your main ui is sketchup, and your dialog pretty often has to reflect state updates from sketchup. How does your example handles that?

Just saw this answer!! :slight_smile:

I’ll try what you suggest. Thanks!

An optional fix is to patch bridge.rb, line 657 as follows.

#value   = dialog.get_element_value("#{NAMESPACE}.requestField") # returns empty string if element not found
value   = @dialog.get_element_value("#{NAMESPACE}.Bridge.requestField") # returns empty string if element not found