Html dialog box to set and get information

I’m trying to send and get some array information from Ruby using htlm dialog. I’m a beginner and I have some problems to understand how they exchange information. I will apreciate a simple example with two arrays; the first array only to show Ruby model values with no permission to change them, and the second array to show Ruby values (even empty) with permission to change them and send them back to Ruby. Thanks.

What is your background and programming ability? Are you fluent in Ruby, HTML, and javascript? Have you studied the SketchUp Ruby API docs and examples?

What I’m driving at is that many of us are reluctant to “do your homework for you” when you haven’t shown us any effort to do it yourself. Take a stab at it and then ask for help with specific issues you encounter in your code.

1 Like

See the pinned list of Ruby Learning Resources that I posted in the Ruby API subcategory

… and please post your Ruby coding questions in the Ruby API subcategory.


This is what we’d call “read only” access.

I have actually been attempting to create just such an example, but it has become too complicated (because of platform differences and language translation issues,) to serve as a novice example.

So I am in need of another kind of SketchUp data for a simpler example.

(1) What kind of “model values” would you want to see but not change ?

(2) What kind of “model values” would you want to be able to edit in the HTML dialog ?

Dear Dan. Thank you for your prompt response. I’ve been reading for a long time many of your posts. It’s a plesure to contact you. Let me explain better. I solved the problem using dynamic attributes (set&get), following the ScottLininger Dec 09, 2009 response about how to access to DA showing the option (“NONE” if consumers can’t see or edit this attribute # = “VIEW” if consumers can see this attribute # = “TEXTBOX” if consumer can enter a value for this attribute # = “LIST” if consumers can select a value from a list). As my values are component values, saved in 2D array, I thought it would be better if I use a table in my own html window. Therefore I needed to define my own attribute option window. So, I created my own html window and I took my Ruby array, but I don’t know how to share my array between Ruby and html (it is not as easy as set&get in DA). Ex: a=[[1,2, ],[9,4, ],[ ,6, ],…] in Ruby, to be sent to html, to be shown in a table (In my html table a[n][0] as VIEW mode, a[n][1] as TEXTBOX mode and a[n][2] as LIST mode if I can send and use b[op1, op2,… ] ). And OK button to close the window and send the array back to Ruby.

You actually send arrays and hashes to the dialog’s JavaScript as JSON text.

It is really easy and Ruby’s JSON library is usually already loaded by SketchUp. But you can be sure at the top of your code by requiring the Ruby JSON library like this …

require 'json' unless defined? JSON
ruby_ary = [ [1, 2, 3 ], [9, 4, 6], [4, 6, 8] ]
# dialog is your UI::HtmlDialog object
json = ruby_ary.to_json
dialog.execute_script( %[var js_ary = JSON.parse('#{json}');] )

Of course the variable names can be whatever you wish, and be the same or different on each side (ie, the “Ruby-side” and the “JavaScript-side”.)

The %[text] (or %Q[text],) is a Ruby double quoted string syntax that does interpolation.
(The %q[text] syntax is a single quoted string that does not do interpolation.)
We use them because JavaScript code often needs to have quotes in it, so these special String syntax allow us to use quotes within them. ( See: Ruby Syntax: Literals - Strings )

Anyway … you can also easily pass more complex and robust data such as a Ruby Hash, Struct or OpenStruct over to the dialog as JavaScript objects.

json = dyna_hash.to_json
dialog.execute_script( %[var my_obj = JSON.parse('#{json}');] )

We use single quotes around the JSON.parse method argument because JSON text uses double quotes internally around strings and member names.

ADD: We can also send SketchUp Ruby AttributeDictionary object data over quite easily because they have Ruby’s Enumerable module mixed in, which gives them a #to_h method to produce a Hash.

# Where dict is a reference to some attribute dictionary:
dict = comp_inst.attribute_dictionary("dynamic_attributes")
json = dict.to_h.to_json
dialog.execute_script( %[var dict_inst = JSON.parse('#{json}');] )
dict = comp_inst.definition.attribute_dictionary("dynamic_attributes")
json = dict.to_h.to_json
dialog.execute_script( %[var dict_defn = JSON.parse('#{json}');] )

(You should of course, check that the attribute dictionary getter calls do not return nil.)

… following on.

Once your data array (or JS object) is on the dialog side, you can dynamically build your table of data using the DOM.

For example after your empty page loads there will be a document.body object that represents the HTML <body> element.

You can add a <div> element to this so that you can begin to build your table inside it …

var myDiv = document.createElement('div');
document.body.appendChild(myDiv);

See:

From those pages there are many links and code examples in each of the JavaScript method pages.
It will take you some time to learn your way around. (There are many free JavaScript tutorials and books on the Web.)

I suggest opening your empty dialog in SketchUp and then right-click it and choose “Show DevTools” from the mouse context menu. The Chrome Developer Tools window will open.

Now you can use the JS Console in the CDT window to try out JavaScript statements and see the result in your dialog. You can also see the HTML document “tree” right there in the Dev tools window.

As a bonus you can also begin to explore how CSS styles affect your dialog’s HTML elements.

:nerd_face:

… Now, all that said …

My actual examples are doing the “heavy work” on the Ruby side using HTML, JS and CSS file fragments and Ruby text strings. I build up the dialog webpage in Ruby using one of 2 Ruby String class parameter replacement methods.

Either … String#% or String#gsub.

Both of these methods will accept a Hash of replacement values that replace all the %{param_name} substrings in the text with the values from the hash with the matching :param_name symbol key.

I do this replacing on JS and CSS file text if I need to set dynamic changing values in the stylesheet and / or the dialog’s JavaScript code.

Then I build up a data list in HTML using a HTML file fragment for one line of data that has several %{} parameters for the data fields. I do this in an iterator, appending each HTML fragment to a list string object.

Lastly before giving the html text to the dialog object, I replace a couple of %{styles} and %{script} parameters in the main HTML file for the dialog, with the CSS and JavaScript code text, as well as replacing the %{datalist} parameter in the HTML’s <div> element where it will “live” in the webpage.

Then I do a dialog.set_html(htmlpage) and show the dialog.

We cannot get away from using some JavaScript as we must create event listeners for button clicks and edit control changes.

It’s clear how to send my array to Java script using JSON. But I still don’t understand how should I get it in Java (same JS array name?) and how should I send it back to Ruby once I check/fill up and close my table in htlm window.

Be careful. JavaScript is not the same as the Java programming language. The two are distinct.

As I said …

There are different conventions for Ruby and JavaScript identifiers, but both are loose and allow most any kind of reference identifier.

JS normally uses var identifiers that begin with a lower case letter and are camelCaseNames (some times called “snake-case”.)
Ruby conventions use CamelCaseClass for class and module names and use variable and method identifiers as lowercase with underscore separators. ie … some_value = my_method("argument")

But then, JavaScript will also not care if you use Ruby-like variable names like my_var either.

But if you are using simple lower case identifiers like ary or data, etc., the names can be the same on both sides and you won’t break any convention.

As it says in the UI::HtmlDialog class documentation for #add_action_callback()

Basic types such as booleans, numbers, strings, arrays and hashes are automatically converted between Ruby and JavaScript.

You just need to try and remember that JavaScript uses Objects for what Ruby calls a Hash. So if you pass a JS Object to a Ruby dialog callback proc, the SketchUp API will convert it to a Ruby Hash.

So … let us say that your data array is named @atts (short for attributes,) on the Ruby side, and you pass it over to the dialog JavaScript named as atts. (JS doesn’t have @vars so you’d omit the @ prefix.)

From your dialog’s JS, you need a button click to send the updated array back to Ruby …

    var applyButton = document.getElementById('apply');

    applyButton.onclick = function() {
        sketchup.receive_data(atts);
    }

* This snippet assumes your HTML has a <button> element whose id is set to "apply".

Then your code must have already attached an action callback to your Ruby dialog object, in order to receive the updates from the dialog. So in Ruby …

def attach_callbacks(dialog)
  dialog.add_action_callback("receive_data") { |_unused, atts|
    # When this gets called from the JavaScript side, ...
    # the block parameter atts will be a Ruby array. So, just make the
    # assignment to the local instance var referencing the array:
    @atts = atts
    # Now close the dialog:
    dialog.close
  }
end

You’ll also probably need a Cancel button, so in the dialog’s JavaScript …

    var cancelButton = document.getElementById('cancel');

    cancelButton.onclick = function() {
        sketchup.close_dialog();
    }

And on the Ruby side a callback to accept this call from JS, so we need to insert a callback attachment to the previous method …

def attach_callbacks(dialog)
  dialog.add_action_callback("close_dialog") { |_unused|
    dialog.close
  }
  dialog.add_action_callback("receive_data") { |_unused, atts|
    # The block parameter atts is now a Ruby array.
    # So just make the reference assignment to the local instance var:
    @atts = atts
    # Now close the dialog:
    dialog.close
  }
end

:nerd_face:

If you test it, you will see that it’s still not working …

_body>
_script>
document.getElementbyID(“r0c0”).innerHTML=js_ary[0][0]
document.getElementbyID(“r1c1”).innerHTML=js_ary[1][1]
_/script>
_h1> Result:
_p id=“r0c0”>


_p id=“r1c1”>
_/body>
_/html>

dialog = UI::HtmlDialog.new(
{
:dialog_title => “Dialog Example”,
:preferences_key => “com.sample.plugin”,
:width => 600,
:height => 400,
:left => 100,
:top => 100,
:style => UI::HtmlDialog::STYLE_DIALOG
})
ruby_ary = [ [1, 2, 3 ], [9, 4, 6], [4, 6, 8] ]
json = ruby_ary.to_json
dialog.execute_script( %[var js_ary = JSON.parse(’#{json}’);] )
dialog.set_html(html)
dialog.show

Please fix your code posting …

I fixed it in my editor ... (click to expand) ...
html = %{
  <!DOCTYPE html>
  <html>
    <head>
    </head>
    <body>
      <h1> Result:
      <p id="r0c0">
      <p id="r1c1">
    </body>
    <script>
      //document.getElementById("r0c0").innerHTML= js_ary[0][0];
      //document.getElementById("r1c1").innerHTML= js_ary[1][1];
    </script>
  </html>
}

dialog = UI::HtmlDialog.new(
  :dialog_title => "Dialog Example",
  :preferences_key => "com.sample.plugin",
  :width => 600,
  :height => 400,
  :left => 100,
  :top => 100,
  :style => UI::HtmlDialog::STYLE_DIALOG
)

ruby_ary = [ [1, 2, 3 ], [9, 4, 6], [4, 6, 8] ]
json = ruby_ary.to_json

dialog.set_html(html)
dialog.show

# Entered individually at the Ruby Console ...
dialog.execute_script( %[var js_ary = JSON.parse('#{json}');] )
dialog.execute_script( %[document.getElementById("r0c0").innerHTML= js_ary[0][0];] )
dialog.execute_script( %[document.getElementById("r1c1").innerHTML= js_ary[1][1];] )


The first error I saw was that the <script> element was put before the <body> where the elements and their IDs were defined. So I moved the <script> element after the <body>.


The second error I saw, after the dialog opened, I opened the Chrome DevTools window (as I told you to,) and switched to the Console tab.

image

So I had to

  1. correct the function names from "getElementbyID" to "getElementById"
  2. put semi-colons at the line of each statement

The 3rd error I saw …

image

There are several reasons why this is happening.

  1. the call to dialog.execute_script is happening before the html is loaded into the CEF window, so it’s JavaScript process is not even loaded at that point in time.

  2. but this error is happening at line 12 which is in the <script> element. When the page is loading the js_ary is not yet defined so at that point during the load, you cannot yet set the values of your <p> elements from the js_ary.

We can instead move those statements to dialog.execute_script calls. …

So commenting out those two statements in the <script> block and loading the dialog, then executing the call to send over the js_ary we no errors in the JS Console and if we type "js_ary" + ENTER we see that the array is indeed defined …

image g)


Then subsequently running these two statements in SketchUp’s Ruby Console …

dialog.execute_script( %{document.getElementById("r0c0").innerHTML= js_ary[0][0];} )
dialog.execute_script( %{document.getElementById("r1c1").innerHTML= js_ary[1][1];} )

… we see the values appear in their <p> elements …

image


Although these statements actually work, you’ll want to define an automated function in JS that you call from Ruby, to iterate the array with a JS for loop to populate your HTML table.


Other observations

  • You cannot put linefeeds into HTML by inserting empty lines in the source text.
    You need to use <br> elements.

  • You need to use properly formed HTML as shown in the example above.

  • Notice in the above Ruby examples that we can use whatever delimiters that make sense for surrounding % strings. Since your JS statements used [] internally, I use curly braces so as not to confuse my editor’s color lexing.

Dear Dan. Many thanks, but it’s still not working in my Ruby Code Editor. “Done running code. Ruby says: Nil result (no result returned)”. If you can check it in your Sketchup Ruby Code Editor, … Sorry about my post format, I couldn’t find how to insert HTML code, so I decided to eliminate semi-colons and then I made some typing mistakes. Automatically, I assumed the Html place was before anything else, and inside it, I’ve always seen the script connected with Ruby on the top. I’m very amazed that you needed to check and change so many things because of order matters in this crazy Ruby-Html-JS triangle. The problems that you found and the need to withdraw sentences from Script to Ruby to keep the order, will appear in many cases and seems to be too complicated to program properly and too costly to fix. And it still remains to face returning data from Html to Ruby… Do you really think that this is the way to do something as simple as showing and checking some attributes from Sketchup in a well-designed Html window?

What editor do you use? I use Notepad++. It’s free and open source.

I think you are using one of the WebDialog based Code Editor extensions.
I’m sorry (to the devs who made them) but I think they are more trouble then they are worth.

When I say paste into the Ruby Console, … I mean the native SketchUp Console.
The other consoles are extensions that can have bugs and warning messages that confuse new coders. I don’t use them when testing or developing as I do not wish to introduce any influences from outside my code.

If you have a separate .html file that you are using for your dialog, then you can paste it into it’s own code delimited block in a forum post, like so …

```html
<!-- html text goes here -->
```
And then post separate Ruby code in it’s block …

```ruby
# Ruby code goes here
```
… and javascript in it’s block …

```javascript
// javascript code goes here
```
… and, etc., etc.

You mean the location of a HTML <script> element ?
It can be in the <head> or within the <body> or after the <body>.

It is okay to have it before the <body>, usually within the <head> element, as long as it doesn’t refer to anything that will be defined later in the document whilst it loads.

Not so. I only pulled them from the <script> tag and showed using them via the Ruby console, to make the point that things need to happen in the correct order.

The main problem with your JS was that it was not being deferred until after the document (and all it’s elements were loaded and it’s data was loaded.)

To defer evaluation, you put your JS code into functions, and then make sure that these functions get called only when things are ready.

There are builtin JS events like body.onload and document.readystatechange. Examples …

EX-1. This JS event handler will call the Ruby dialog object’s "ready" action callback method.

// JavaScript

    document.onreadystatechange = function () {
        if (document.readyState === 'complete') {
            // Call the Ruby side to tell it the dialog's 
            // webpage is completely rendered and ready:
            sketchup.ready();
        }
    }

EX-2: This JS event handler is from an example I’m doing currently.

I inject into the dialog JS a hash of model properties (:name,:description,:tags) as a JS Object named “data” so it’s already there. At the top of my dialog.js file I have the data Object definition:

// JavaScript

    // The values for these data properties will be
    // replaced by using Ruby's format string replacement.
    var data = {
        name: "%{name}", description: "%{about}", tags: "%{tags}"
    }

(There are more JS functions but they’d just distract from this example.)

Then in Ruby, I read the dialog.js file directly into a Ruby String …

# Ruby
      # Get the JavaScript text.
      def javascript
        # Read the JavaScript "dialog.js" file:
        File.read(File.join(__dir__,'dialog.js'))
      end

… and then replace the text properties in the JS text with values from Ruby …

# Ruby
        # Get the JavaScript text and replace the %{} parameters
        # with the matching values in a properties hash:
        js_code = javascript() % Hash[
          :name,  model.name,
          :about, model.description,
          :tags,  model.tags
        ]

This snippet above is using the Ruby core String#% method, which is replacing the %{} parameters in the text returned by the javascript() method (shown above.) If it looks confusing perhaps if I simplify it a bit …

# Ruby

# The hash with replacement values:
properties = {
  :name  => model.name,
  :about => model.description,
  :tags  => model.tags
}

# The javascript text before replacement:
js_code_before = File.read(File.join(__dir__,'dialog.js'))

# Replace the %{} parameters in the javascript text
#   with values from the properties hash:
js_code_ready = js_code_before % properties

# The "js_code_ready" text is ready to be inserted into the HTML text:

I then repeat this above also with the HTML text, which itself has
embedded %{script}% and %{styles} replacement parameters …

# Ruby
        # Get the HTML text and replace the %{} parameters with
        # the matching values in a hash containing references
        # to the text for the <style> and <script> elements:
        html_text = html() % Hash[
          :script, js_code_ready,
          :styles, stylesheet()
        ]

(Sometimes I also do replacement into a CSS stylesheet file’s text.)

Now for this simple tutorial example, I instead could have stuffed the values directly into the HTML text elements where they’ll be editable by the user. But I wanted to show the use of a JS Object to hold the data because it can be passed as is, as a whole back to the SketchUp Ruby dialog object, and SU’s API will convert it to a Ruby hash.

Anyway … once the HTML text object is ready it is given to the dialog object and the dialog is shown so the webpage and it’s JavaScript loads …

# Ruby
        # Now set the dialog's HTML to the complete text:
        wdlg.set_html(html_text)
        wdlg.show

Then when the body of the HTML document is done loading, the values from the “data” JS object are assigned to their corresponding edit control elements whose ids are modelName (an <input type="text"> element), descriptiveText and tags (which are <textarea> elements.)

// JavaScript

    document.body.onload = function() {
        modelName.value = data.name;
        descriptiveText.value = data.description;
        tags.value = data.tags;
    }

At this point the dialog is ready for users to edit the properties displayed in the edit controls.

I already showed how EASY that is above in post 9.

But I’ll continue with snippets from my current tutorial project …

When the user edits one of the properties the JavaScript must react and take the value of the HTML element and stuff it back in the data object.

// JavaScript

    // Edit Controls
    var modelName = document.getElementById('model');
    var descriptiveText = document.getElementById('description');
    var tagsList = document.getElementById('tags');

    modelName.onchange = function() {
        data.name = this.value;
    }

    descriptiveText.onchange = function() {
        data.description = this.value;
    }

    tagsList.onchange = function() {
        data.tags = this.value;
    }

NOTE: The JavaScript keyword this in each event handler function block refers to the object upon which the handler’s event was fired. So in the last onchange event, this refers to tagsList, which is a var (reference) that refers to the document’s HTML element whose ID is "tags".

Then when the user is satisfied, they can click the “Apply” button and the properties will be sent over to Ruby where they’ll be assigned to the model’s properties via API setter methods …

// JavaScript

    // Buttons
    var applyButton = document.getElementById('apply');
    var cancelButton = document.getElementById('cancel');

    applyButton.onclick = function() {
        sketchup.receive_data(data);
        // Let the Ruby side close the dialog after
        // it processes the updated property values.
    }

    cancelButton.onclick = function() {
        sketchup.close_dialog();
    }

And on the Ruby side we’ve already attached the callbacks that will get called by the JavaScript side …

# Ruby

      def attach_callbacks(dialog)

        dialog.add_action_callback("receive_data") { |_unused, updates|
          # The passed JS Object "data" is now a Ruby hash "updates":
          updates.each do |key,value|
            # Call the model's setter method for this
            # key passing along the value to be set:
            dialog.model.method("#{key}=").call(value)
          end
          # Close the dialog:
          dialog.close
        }

        dialog.add_action_callback("close_dialog") { |_unused|
          dialog.close
        }

      end

# Note: My code is actually using a dialog subclass that has it's
# "owner" model as an accessor instance variable. Ie: dialog.model()

My example property setting block works because I choose parameter names that are the same as the Ruby API model property method names. I could just as easily do …

# Ruby

        dialog.add_action_callback("receive_data") { |_unused, updates|
          # The passed JS Object "data" is now a Ruby hash "updates":
          model = Sketchup.active_model
          model.name= updates[:name]
          model.description= updates[:description]
          model.tags= updates[:tags]
          # Close the dialog:
          dialog.close
        }

And that is how simple it is to send stuff back from the JS side to the Ruby side.

Well, the learning curve is certainly steep. I came to SketchUp and Ruby already certified in HTML, JavaScript and CSS, so I was way ahead of the game.

1 Like