Question about htmldialog and pushing variables from Ruby to Javascript

Just getting into Ruby for SketchUp, and the first tool I’m trying to develop is a mass view exporter, which would require user input to choose a list of scenes to export from, and a dynamic form reading the layer info inside the model. I got the scene exporting part figured out, but the UI is a bit confusing at first given it’s actually html+js, and it’s at its most confusing when I try to figure out how to push for example a list of model layer names as array into Javascript so I can dynamically generate a multi-select form in the html window.

The script below that I wrote kind of works, but it feels very hard coded and I’m posting here to see if I’m understanding HtmlDialog.add_action_callback and HtmlDialog.execute_script correctly, and if there are far more efficient and elegant ways to do this.

Thank you! (And also how do you post code in here? There is only blockquote and pre-formatted text Thnx I got it!) And if you have better htmldialog example scripts, please share!

module Example
    model = Sketchup.active_model
    model_layers = model.layers
    model_scenes = model.pages
    scene_names = []

    model_scenes.each {|scene| scene_names.push(scene.name)}

    html = <<-EOT
    <p>Choose (multiple) layers</p>
    <select id = "layer_select" name="ok" size="12" multiple>
    </select>

    <script>
    sketchup.views_to_form();
    </script>
    EOT

    options = {
      :dialog_title => "Choose Views to Export",
      :width => 300,
      :height => 400,
      :style => UI::HtmlDialog::STYLE_UTILITY
    }
    dialog = UI::HtmlDialog.new(options)

    dialog.add_action_callback("views_to_form") {|action_context|
      js_command = "var new_text = '';"
      scene_names.each do |name|
        js_command += "new_text += '<option>#{name}</option>';"
      end
      js_command += 'document.getElementById("layer_select").innerHTML = new_text;'
      dialog.execute_script(js_command)
    }

    dialog.set_html(html)
    dialog.center
    dialog.show
end

To post code, precede your code on a separate line by three backticks and the word ruby

End it, again on a separate line, by three backticks.

 ```ruby
 ... ruby code here... 

` ` `
three backticks at the end, but I can’t format them here to display properly inside the ruby code block - I used the html code #96;to show them and that only works outside the block.

If I type two, they show, but when I type a third, they all disappear.

They are all wrapped in

```text
...plain text here...

` ` `
Again, I tried to show the three backticks at the end of the text wrapper, but only two ticks or none show.

There must be a way to ‘escape’ the backtick characters but I can’t see how. It isn’t either \ or /.

Searched the forum for ‘how to post ruby code’ but nothing relevant came up in the dozen or so results.

```ruby
module YourCode
# Your code here
end
```
Gives this:

module YourCode
   # Your code here
end

So your (unchanged) code would look like:

model = Sketchup.active_model
model_layers = model.layers
model_scenes = model.pages
scene_names = []

model_scenes.each {|scene| scene_names.push(scene.name)}

html = <<-EOT
<p>Choose (multiple) layers</p>
<select id = "layer_select" name="ok" size="12" multiple>
</select>

<script>
sketchup.views_to_form();
</script>
EOT

options = {
  :dialog_title => "Choose Views to Export",
  :width => 300,
  :height => 400,
  :style => UI::HtmlDialog::STYLE_UTILITY
}
dialog = UI::HtmlDialog.new(options)

dialog.add_action_callback("views_to_form") {|action_context|
  js_command = "var new_text = '';"
  scene_names.each do |name|
    js_command += "new_text += '<option>#{name}</option>';"
  end
  js_command += 'document.getElementById("layer_select").innerHTML = new_text;'
  dialog.execute_script(js_command)
}

dialog.set_html(html)
dialog.center
dialog.show
1 Like

You have to escape each backtick individually
\`\`\`ruby
module YourCode
# Your code here
end
\`\`\`

To show this
```ruby
module YourCode
# Your code here
end
```

to show the first one here you have to escape the backslashes too.

Thank you and where did you find information like this? Also trying to figure out how to strike through texts.
Also do you have any insight on the main question of my post?

The escaping was trial and error.

I’m no expert on using HtmlDialog.

Strikethrough with two tildes before and after like this:
~~Strikethrough~~

1 Like

There are 2 options for getting the arrays over to the JS.

Use JSON (which describes as JavaScript object and can therefore be used to define a JS object.)
It is likely that SketchUp’s native extensions have already loaded Ruby’s JSON library, but just to be sure you should add a dependency require ta the top of your file …

require 'json'

a. Send it when the page is ready via a callback …

dialog.add_action_callback("views_to_form") {|action_context|
  json_text = Sketchup.active_model.pages.map(&:name).to_json
  dialog.execute_script("var scenes = #{json_text}; load_scene_select();")
}

… or …

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

The examples assume a couple of JS functions to load the array values once on the JS side into the appropriate <select> HMTL element.

ADD: Also note that these examples are using the #map method which comes from most of the API collection classes mixing in the Ruby core Enumerable module. (At the top of the API class doc pages will be a “Includes” box listing any libraries mixed into the class.)


b. But, as you are actually creating the HTML for the dialog dynamically on the Ruby side, you can just stuff the values into the text, before setting the HTML for the dialog object.

This way leverages Ruby String interpolation. See primer:
https://ruby-doc.org/core-2.5.5/doc/syntax/literals_rdoc.html#label-Strings

    # Map the active model's layer collection to an array of HTML option elements:
    layers_options = Sketchup.active_model.layers.map do |layer| # << -- fixed
      name = layer.name # << -- fixed
      %[<option value="#{name}">#{name}</option>]
    end
    # Join the array of options into a newline separated text string:
    layers_html = layers_options.join("\n")

    html = <<-EOT
    <!DOCTYPE html>
    <html>
    <body>
        <p>Choose (multiple) layers</p>
        <select id = "layer_select" name="ok" size="12" multiple>
            #{layers_html}
        </select>

        <script>
            sketchup.views_to_form();
        </script>
    </body>
    </html>
    EOT

Yes, that is right. HereDocs can use String interpolation.


All this aside … your example module is written in a sequential script-like way.

SketchUp embedded Ruby is event-driven. So you’d have a mneu item or toolbar button that will fire off a Ruby method that will build and display your dialog.

You do not use local variables inside modules or classes. If you need persistence, use @vars.
Otherwise you pass values between methods as arguemnts.

The references for the active model and it’s collections are never set in stone as you have done.
They are only referenced at the last second after the user has chosen a command. You need to realize that on the Mac it’s a multi-document interface where the user can switch back and forth between numerous open models.

2 Likes

Nice, I didn’t know that.

Yea, they are basically a big multiline double quoted string.

But I don’t like the delimiter format myself, (forget how to use it usually) … so I just use a multiline % delimited string like …

    html = %[
    <!DOCTYPE html>
    <html>
    <body>
        <p>Choose (multiple) layers</p>
        <select id = "layer_select" name="ok" size="12" multiple>
            #{layers_html}
        </select>

        <script>
            sketchup.views_to_form();
        </script>
    </body>
    </html>
    ]

these are very clear and helpful, especially the string interpolation used in HereDocs, that does feels much better.
Your last comment is very valid, I imagine this script will not work well if the user creates a new layer after the script has already started running.
Not sure how the Mac system affect all of these yet, will have to do some research on that.
Thank you!

I didn’t notice anything in @DanRathbun’s suggestions that should be OS dependent or sensitive. Of course, those are famous last words so it should be checked.

1 Like

am I understanding correctly that this here should actually be like below, or does the above also work?

layers_options = Sketchup.active_model.layers.map do |layer|
  %[<option value="#{layer.name}">#{layer.name}</option>]
end
1 Like

Dialogs are messy, and what is done on the Ruby side vs what is done on the JS side depends on what you’re familiar with. You can use dialog.execute_script to call a JS function that loads whatever multi-select element you’re using in the html. Typically, this would be using DOM JS functions.

I’ve used arrays of arrays (using #inspect) as the JS function argument to generate tables with checkboxes, color spans, and the text/name. Hence, you may not even have to pass as JSON.

The advantage to using DOM is that a non-modal dialog can be updated. You can also pass a doc html fragment and parse it in JS, but that seems tedious.

Also, not sure about current SU, but dialog.execute_script may have limits on the string length, and passing an html fragment adds to the string length.

1 Like

YES! :blush: (Brain fartitis) And no, I made a mistake.

I was thinking at first to map to an array of layer names, then do a loop stuffing the names into the HTML strings. (That’s twice today I was pondering two different ways, and wound up expressing a mixture that doesn’t work. Must be the meds I’m on. :roll_eyes:)

You’ll find that Ruby is multi-paradigm. There’s many ways to do a task. For example …

layers_options = Sketchup.active_model.layers.map(&:name).map do |name| 
  %[<option value="#{name}">#{name}</option>]
end

… or …

layers_options = Sketchup.active_model.layers.map do |layer|
  name = layer.name
  %[<option value="#{name}">#{name}</option>]
end

Ie, the value used in the mapped output array, is the last evaluated value from the block.

This what I was referring to in (a) above

The JS function "load_layer_select()" would be the function using DOM to populate the selection elements.


You may find some JS framework that builds a nice interface for you, which might require the selection values to be in a JS array. Although I showed stuffing the values into the HTML <option> elements, you could also stuff the array as JSON into the <script> element in prep for use by some JS GUI framework.
Ex …

json_layers = Sketchup.active_model.layers.map(&:name).to_json

# Down in the HEREDOC ...

    html = <<-EOT
    <!DOCTYPE html>
    <html>
    <body>
        <p>Choose (multiple) layers</p>
        <select id = "layer_select" name="ok" size="12" multiple>
        </select>

        <script>
            var layer_names = #{json_layers};
            // Then call some JS framework to bind this array to the
            // #layer_select element as child option elements.
        </script>
    </body>
    </html>
    EOT

Some frameworks require the data array(s) to be wrapped within an object (instead of at the top level as shown.) But this is not any more difficult.

1 Like

I was just reading about & shorthand, great to look at a SU example here. I did not know shorthand can be further chained, this is really cool!