Alternative to get_element_value method for HtmlDialog

Has a best practices alternative method been set for transferring values from a dialog to an extension? I was storing values in hidden inputs and moving those to the extension with the get_element_value method on the WebDialog.

1 Like

Yes.

In the UI::HtmlDialog class, the add_action_callback() instance method will convert between Js and Ruby types automatically.

In the old UI::WebDialog class, your webpages could only send String data via a protocol handler.

The UI::HtmlDialog class does not use the protocol handler to send data to Ruby.


That said, you could still likely do it, thus defining a singleton method upon your HtmlDialog instance:

EDIT: Proven not to work! The UI::HtmlDialog class’ execute_script() method returns nil.

Non-working attempt ...
@dlg.define_singleton_method(:get_element_value) {|id|
  self.execute_script("document.getElementById('#{id}').value")
}

Or, you can create your own subclass of UI::HtmlDialog, thus:

module Archetris
  class NiftyDialog < UI::HtmlDialog

    def get_element_value( id )
      self.execute_script(
        "document.getElementById('#{id}').value"
      )
    end

  end
end

EDIT: For a working example see …

3 Likes

Thanks, Dan. That’s incredibly helpful.

@Dan: question is though: is it best practice to continue storing values in hidden elements OR just use the callback to send data from JS to Ruby instead?

The storing of values, and whether they are hidden or visible is not really the issue anymore. (You can still have hidden values, stored in hidden html elements, or even better stored in Javascript objects.)

It is the passing of values to the Ruby side is the more important concept.

So yes, best practice going forward will be to use the new sketchup Javascript object, which will convert on the Ruby side to the correct class of Ruby object.
No more chopping up big strings into little strings and then convert the strings to arrays and numbers, etc.

I just showed the above code as a workaround for getting old code to work without a large overhaul.

1 Like

the method #execute_script is defined to return nil

Yes, and the API docs are never wrong … :rolling_eyes:

If the above does not work,… then try something like this:

EDIT: This does not work either, See below.

(Collapsed as this example will also not work.)
module AuthorTest

  @@loaded ||= false

  class NiftyDialog < UI::HtmlDialog

    def initialize(*args)
      super(*args)
      @ready ||= {}
      @returned_values ||= {}
      add_action_callback('returned_value') {|ac,id,value|
        @returned_values[id]= value
      }
      add_action_callback('element_value_ready') {|ac,id|
        @ready[id]= true
      }
    end

    def get_element_value( id )
      @ready[id]= false
      self.execute_script(
        "sketchup.returned_value('#{id}',document.getElementById('#{id}').value,
          { onCompleted: function() {
              sketchup.element_value_ready('#{id}');
            }
          });"
      )
      until @ready[id]
        sleep(0.1)
      end
      @returned_values[id]
    end

  end # custom HtmlDialog subclass
  
  def self::open
    self::reset()
    
    @dlg = NiftyDialog::new(dialog_title: "Test")
    
    @dlg.set_html('
      <!DOCTYPE html>
      <html>
        <head>
        </head>
        <body>
          <div id="DataField">
            Testing 1, 2, 3, ...
          </div>
        </body>
      </html>
    '
    )
    @dlg.show
    sleep(1.0)
  end

  def self::test
    msg = "Getting the 'DataField' element's value ..."
    msg<< "  value is: \"#{@dlg.get_element_value('DataField')}\""
    puts msg
    UI.messagebox(msg,MB_OK)
  end

  def self::reset
    @dlg.close if @dlg && @dlg.visible?
    @dlg = nil
  end

  if !@@loaded
    sm = UI.menu("Plugins").add_submenu("Get Element Value Test")
    sm.add_item('Open Dialog') { self::open }
    sm.add_item('Test') { self::test }
    sm.add_item('Reset') { self::reset }
    @@loaded = true
  end

end

EDIT: For a working example see …

Yep. This is true for the UI::HtmlDialog class. Seems like it used to work for the old UI::WebDialog class.

I just tested and it seems now (with the UI::HtmlDialog class,) there is no way to get an immediate synchronous value of a HtmlDialog element. Control must return to the SketchUp engine before the JS to Ruby callbacks will fire. So then there is no way, that a custom get_element_value() method could ever itself return the value, if it is the instigator of the JS call to send the value to Ruby.

get_element_value_test.rb (2.6 KB)

This is unfortunate as that ol’ get_element_value() method came in very handy.

What’s the workaround ?

It seems that you’ll need to make a call to fire a method on the JS side to collect ALL values that you might wish to test on the Ruby-side, and pass them en masse to Ruby as a array, etc. Or,… you move this kind of immediate comparison expression to the JS side of your code.

EDIT: For an asynchronous working example see …

Hi Dan. I have been able to get the UI::htmlDialog to work using your pattern. And I get the appropriate information coming back from the javascript Sketchup.receiveValue(arg1, arg2). That works. The objective of this routine for now is to return a selected directory path. Just before process_value I’m attempting to get a list of directories/files within the path returned, but no matter what method/block/proc/ whatever, it is always returning an error stating that it undefined method.

I’m trying to call a method within the Module namespace that contains the Dir(path). These methods exist outside of the Class which contains the UI.htmlDialog. So how do I reference methods outside of the class methods defined by your example that exist outside of the htmlDialog instance?

Thanks,

Scott.

class MyToolsDialog < UI::HtmlDialog

def attach_callbacks
  add_action_callback('receiveCmd') do |not_used,id,val|
    receiveCmd(id,val)
    UI.messagebox("Inside add_action_callback(receiveCmd, directly from MyTools_Jscript.js")
  end
end

def get_element_value(id)
  #return unless id.is_a?(String)
  execute_script("sendValue('#{id}');")
end

def receiveCmd(htmlCmd,htmlParam)
  UI.messagebox("Inside receiveCmd, directly from add_action_callback()")
  process_value(htmlCmd)
  process_value(htmlParam)
  UI.messagebox("Inside receiveCmd, just before get_dir_list(#{htmlParam})")

THIS IS WHERE IT ERRORS OUT

  get_dir_list(htmlParam)

end

def process_value(id)
  # Do something with @values[id]
  puts id
end


def show(*args)
  attach_callbacks()
  super
end

def show_modal(*args)
  attach_callbacks()
  super
end

end # custom HtmlDialog subclass

def get_dir_list(x_dir)
UI.messagebox(“Inside get_dir_list #{x_dir}”)
puts listFiles(x_dir)
end

def listFiles(x_dir)
UI.messagebox(“Inside listFiles(#{x_dir})”)
xDirList = “”
UI.messagebox(“xDirList initialized…”)
xList = Dir.entries(x_dir)
UI.messagebox(“Dir.entries executed… #{xList}”)
xList.each {|fname|
if (xDirList.length() > 0) then
xDirList = xDirList + “,”
end
xDirList = xDirList + “#{fname}” unless fname.start_with?(‘.’)
}
UI.messagebox(“listFiles: returning xDirList = #{xDirList}”)
return xDirList
end

The first 2 puts prints the values returned from the javascript function.

I was elated to get the communications back into Sketchup

DirList

------------ is the command that I receive the htmlDialog javascript routines

------------ that I want to trigger the Directory List for the returned path

D:/my source directory name/Business Projects/SketchupProjects
Error: #<NoMethodError: undefined method get_dir_list' for MyTools:Module> C:/Users/Owner/AppData/Roaming/SketchUp/SketchUp 2017/SketchUp/Plugins/myRubyTools.rb:62:in receiveCmd’
C:/Users/Owner/AppData/Roaming/SketchUp/SketchUp 2017/SketchUp/Plugins/myRubyTools.rb:47:in block in attach_callbacks' SketchUp:1:in call’

Can you explain what’s happening and why it is not calling the appropriate method?

Please reformat (edit) the above post, following the instructions on posting code in the forum.
(And have your editor replace tabs with 2 space characters.)


Do not use UI.messagebox to debug code.

  1. It can fail silently if the evaluation of the string fails.
  2. It is modal and freezes SketchUp’s Ruby, and this causes weird behavior especially in event driven callbacks.

Instead, use puts() to output info to the Ruby Console.

Some random tidbits …

xDirList = xDirList + "#{fname}"

… is a silly way of making Ruby do unneeded work.
fname is already a String object. It evaluates the same as …

xDirList = xDirList + fname

xDirList = xDirList + ","
# ... and ...
xDirList = xDirList + "#{fname}"

Just use the String#<< (append) method … if changing the original string is what you want to do.

xDirList << ","
# ... and ...
xDirList << fname

unless fname.start_with?('.')

If you do not want the '.' and '..' files, then use Dir.glob or Dir[] instead …

xList = Dir["*.*"]
# ... or ...
xList = Dir::glob("*.*")

The return from these two Dir class methods, is an Array of pathnames.
If you want one long string of the names separated by the Locale list separator character, try the Array#join method …

listing = xList.join(Sketchup::RegionalSettings.list_separator)

The separator will NOT be a comma where commas are used as the decimal separator character.


Lastly are you assuming any certain current working directory ?

Yes it appears you haven’t yet learned basic Ruby.

(1) ALL your code needs to be within a top level author / company namespace module.
This means the class and your library module, and your plugins submodules.

(2) To call a module method in another module you need to qualify the method call.

MyNamespace::MyTools::get_dir_list(htmlParam)

… and you must make the module method available from outside by either declaring it as a module method …

module MyNamespace
  module MyTools

    def self.get_dir_list(x_dir)
      puts "Inside get_dir_list #{x_dir}"
      puts listFiles(x_dir)
    end

  end
end

… OR extend the module with itself, creating module method copies of instance methods …

module MyNamespace
  module MyTools

    extend self

    def get_dir_list(x_dir)
      puts "Inside get_dir_list #{x_dir}"
      puts listFiles(x_dir)
    end

  end
end

I would recommend against implementing something like the above get_element_value for HtmlDialog — because it does not do what its name says, it does not get and return anything. Moreover the whole construct with callback is split over three methods, hardly understandable and maintainable. Actually you would rather want to have the callback together with get_element_value than separate (either as callback function parameter or as callback that can be registered on a returned promise).

Better get used to the new concept of asynchronous dialogs. Even the web is asynchronous and works well. Think of the dialog (client) sending to or requesting data from SketchUp (server).


And please put your code in code blocks (edit, </> button). That the code turns out so messy (bold, different size, characters missing) is a direct consequence from that the forum software thinks it can interprete it is markup.

I was thinking along the same lines yesterday, that the method is misnamed, as it’s not a “getter” method.

It is more of a “asynchronous trigger” method. Ie …

In the linked example, perhaps it should be named request_element_value(id) ?
(EDIT: The above method rename was done in the example.)

However, what this was aimed at, was to have old code work for both UI::WebDialog and UI::HtmlDialog.
BUT, it seems this is not really possible unless the code was designed previously to work asynchronously.

So really this was a testing / proof of concept, that proved the concept incompatible with UI::HtmlDialog.

Hi Dan. I was able to get the entire communications between Sketchup and HtmlDialog to work seemlessly. Thanks your assistance.

One more minor detail that has got me stumped. I now have an active HtmlDialog. As I create the new instance of the dialog with HtmlDialog.show, I would like to close it when one of the options in the file passes back something to Sketchup, then automatically close the dialog box.

I have tried the close (where @dlg is assigned to the new instance for the HtmlDialog class) but none of these options are working.

I have a select statement with the command option to close that gets processed upon the return from the Html file.

    when "Close"
		UI.messagebox("Close was sent...")
		@dlg.close
     end

This snippet is within the HtmlDialog class

def close
	@dlg = nil
end

What I’m needing is a way to close the HtmlDialog window within the Ruby code. Everything that I have searched always brings me back to the .close, and this isn’t working for me.

Thanks,
Scott.

HtmlDialog closing by itself

I think that thread’s problem was a different one (it was about an undesired closing because of a non-persisting local variable).

but none of these options are working

That is not helpful. “Not working” can mean everything and nothing. It should close the dialog, what exactly are you doing and what exactly happens instead of what you expect?

Also, in any callback you should (if not to say must) wrap the whole callback in a

@dlg.add_action_callback("callbackName"){ |action_context, *args|
  begin
    ...
  rescue Exception => error
    $stderr.write(error + "\n")
  end
}

clause because since not invoked from the console, exceptions won’t be automatically redirected to the console. Possibly your calback just throws an exception that you don’t know about.

FYI, this doesn’t actually create the Ruby-side dialog instance (the ::new constructor method does this,) …
… the show method creates the platform C-side window object, that hosts the CEF process.


(1) I do not think it is wise to use the same identifier for the callback and a local instance method.

In the original post, you’re using "receiveCmd" as the identifier for both an action callback and an instance method called from that same callback.

Also camelcase on the Ruby-side is for modules and classes. Methods are all lower case, with words separated by underscores.


(2) Again, you are not following the instructions given above, and still do not understand basic ruby.

If the above snippet, is within a add_action_callback block, that is within the attach_callbacks method in the subclass definition (for the MyToolsDialog subclass,) … then self is implied as the receiver object, not @dlg (which is external to the subclass definition.)

SO , …

  # within MyToolsDialog subclass definition ...

  def attach_callbacks
    add_action_callback('receiveCmd') do |not_used,id,val|
      puts("Inside add_action_callback(receiveCmd, directly from MyTools_Jscript.js")
      receive_command(id,val)
    end
  end

  def receive_command(id,val)
    case val
    when "This"
      do_this(id,val)
    when "That"
      do_that(id,val)
    when "Close"
      puts("Close was sent...")
      close()  # <<<<----<<<<< self is implied as the receiver object
    else
      # whatever ...
    end
  end

NO, it is not.

But if you meant within the MyToolsDialog subclass …
Then do not ever override super class methods without calling super from the subclass override method.

(Refer to the subclass override methods in my example, where I make sure to call the superclass’ method within the subclass override.)

If you wish to mark the current Ruby-side instance of the MyToolsDialog dialog object as ready for garbage collection, then …

externally (from your subclass definition) add a block to the @dlg instance’s set_on_closed callback …

@dlg = MyToolsDialog.new(options)
@dlg.set_on_closed { @dlg = nil }
@dlg.show