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

12 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

salut tout le monde
en relation avec ce sujet ,j’ai une question , au fait comment je peu faire passer un tableau de donnée , on va dire [param1, param2,…] à une fonction javascript par execute_script, sachant que ce tableau est dynamique . merci d’avance

# create param1, param2, ... using ruby then send by wrapping in "#{...}"
my_dlg.execute_script( "myJSfunction(#{ [param1, param2, ...] })")

john

1 Like

Salut,
je ne suis pas sur si cette question est liée au sujet ou plutôt générale. Ce sujet n’est pas sur l’utilisation de execute_script mais sur une alternative (ne pas utiliser execute_script).

En général en dévéloppement d’interfaces utilisateur, si les données se changent, il faut notifier l’interface. Qu’est-ce que ça veut dire, un tableau de données (en Ruby)? Si c’est simplement une fonction qui est appelée de nouveau et qui renvoie de nouveaux résultats, il faut passer les résultats et appeler une fonction qui actualise l’interface. Si le tableau est un objet (class Table) il y a des techniques plus avancées (observateurs).

Je créerais une fonction publique JavaScript (par exemple function updateTable()) que je pourrais appeler du côté de Ruby avec les nouvelles données. Cette fonction JavaScript a accès à une référence à l’objet <table> et soit rafraîchit des cellules individuelles, soit supprime le tableau entier (ou toutes les lignes <tr>) et les crée à nouveau (plus facile à réaliser).

Il existe des cadres JavaScript (React.js, Vue.js etc.) qui permettent de mettre à jour uniquement les éléments nécessaires de l’interface utilisateur lorsqu’un objet JavaScript change. Dans ce cas, il suffit de mettre à jour l’objet JavaScript avec les données de Ruby.

En utilisant execute_script:
(Mais attention à tous les dangers, comme échappement de guillements etc. dans des chaînes de charactères)

dialog.execute_script("updateTable(#{[[1, 16.0], [2, 32.0], [3, 8.0]]})")

En utilisant SketchUp Bridge (qui s’occupe de la sérialization et échappement des données):

@bridge.call("updateTable", [[1, 16.0], [2, 32.0], [3, 8.0]])

salut Aerilius
merci pour votre réponse mais malheureusement j’ai pas trop saisi, je pars sur des exemples simples
voila l’exemple que j’ai , (le tableau est dynamique des fois y a un seul élément et des fois n éléments
‘’’ arr =[“h”,“b”,“c”]
js_command = “send (arr)”
dlg_html.execute_script(js_command)
‘’’
du coté java
‘’’ function send(arr)
{
alert( arr)
}‘’’
j’ai testé cette exemple ça marche en parti du coté java la fonction m’affiche seulement le premier paramètre du tableau
, mon grand souci c’est comment créer la fonction du coté java afin qu’elle prenne tout le tableau .
merci d’avance

merci john
mais du coté java comment faut ficeler la fonction afin de recevoir tous les fonctions
merci

Peut-être que l’exemple n’est pas complet? "send(arr)" peut vouloir dire "send(#{arr})".

Il n’y a pas du Java ici. C’est entièrement d’autre chose.

Bonjour Aerilius
désolé je me suis trompé en écriture , je parle bien du javascript , au fait la contion javascript que j’ai crée ne reçoit seulement que le premier élément du tableau “arr” , j’ai créé un alert afin de savoir les élément reçu par la fonction "function send(){ alert(arr)} " mais malheureusement y a toujours un seulement élément , c’est pour cela je demande si y a pas une chose spécifique à faire afin que je puisse récupérer tous les éléments de tableau sachant que le nombre des éléments du tableau est variable
Merci

Ça depend au code. Je n’ai pas vu le code. Une possibilité comment créer une telle situation, c’est par example passer le tableau comme plusieurs paramètres, mais récupérer seulement un seul paramètre (en pensant que ce soit le tableau):

arr = ["h","b","c"]
script = "send(\"#{ arr.join("\", \"") }\")"  # send("h", "b", "c")
dialog.execute_script(script)
function send(arr, param2, param3) {
    alert(arr.constructor.name); // String, not Array
    alert(arr); // "h"
    alert(param2); // "b"
    alert(param3); // "c"
}

Et encore une fois, si tu n’utilise pas SketchUp Bridge c’est un sujet qui lui est propre. Il y a des gens qui s’attendent à trouver ici des réponses liées au sujet. Pour ne pas les brouiller il vaut mieux cliquer + New Topic.

Merci beaucoup
Je vais mettre un code entier pour comprendre au.mieux mon souci

Merci

Great work.

I feel like I should give some credit, since your Bridge library is saving me a ton of work. I also use Ruby Console+, and Attribute Inspector extensively. Thanks for your contribution to the community.

2 Likes

+1000!

2 Likes

It would be easier to use the JSON module for that.

require 'json'

arr = ["h","b","c"]
json = ::JSON.dump(arr)
dialog.execute_script("send(#{json})")

The great part of this is that on the javascript side the parameter comes in as an object, or array in this case.

Also, regarding calling a javascript function from ruby, assing it to the window object:

window.fromSketchUp = (argument) => {
  alert(argument.name);
}

and in ruby:

require 'json'

arg = {
 name: "some name",
 id: 1
}

json = ::JSON.dump(arg)
dialog.execute_script("window.fromSketchUp(#{json})")

I think this solves:

Good tip!
This is basically what the library is doing for Ruby→JS (besides promises). It stems from the time when neither json nor HtmlDialog were available and JS→Ruby lost characters or complete messages).

As you can see from the WebDialog questions inbetween, there will always be developers who built their strings and struggle as long as SketchUp does not make the JSON.dump and execute_script a single API method.

1 Like

For many Ruby core classes, we can also do …

arr.to_json

Which also means, since JSON is the definition of a JS Object you can use it to define the array on the JS side using an identifier …

dialog.execute_script("var arr = #{json};")
1 Like