Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dealing with type conversion errors on accept #16

Open
DanRathbun opened this issue Oct 1, 2021 · 2 comments
Open

Dealing with type conversion errors on accept #16

DanRathbun opened this issue Oct 1, 2021 · 2 comments

Comments

@DanRathbun
Copy link
Contributor

Dealing with type conversion errors on accept.

When the user presses ENTER or clicks the "Accept" button, the app's "accept" function fires and passes the input values to the Ruby "accept" callback. The Ruby callback proc then passes this values array to the type_convert() method for conversion (from JS strings) back into the correctly typed Ruby objects.

But handling of incorrectly entered values by the user is needed.


Future JS side validation

NOTE: Down the road another "subproject" is to implement validation within the Vue JS form, so we need not go back and forth to Ruby - back to JS - repeat, ... etc.


String

No problems with string values. They remain as strings on both sides (Ruby and JS.)


Float and Integer

Currently if the user incorrectly enters values for Float or Integer, Ruby's conversion methods just default to 0.0 and 0 respectively. I will be looking at testing the values using regular expressions for these 2 situations. Something like:

value =~ /^(\d+\.?\d*|\.\d+)/

In testing, I also think it might be best to run the integer values though a float ... ie ...

value.to_f.round

... see next post regarding Length value conversion ...

@DanRathbun
Copy link
Contributor Author

DanRathbun commented Oct 1, 2021

Length

Length values present a challenge because of it's special API nature and specific string notation required to successfully be converted back into a proper Ruby Length object via String#to_l

When the user makes a mistake and there are unrecognized characters in the value that String#to_l cannot understand, it raises an ArgumentError with a message like 'unable to convert "#{value}" to Length'.

In this situation, the results are not set to anything because the mapping loop in type_convert() was interrupted.
The following window.close statement is also not evaluated as the exception breaks out of the proc.

So the user is left with the inputbox dialog doing nothing from their perspective. It just sits there with all the values entered as is and no information as to why.

So what I have done is to wrap the conversion statements within type_convert() in a begin ... rescue .. end block to trap these errors. When an error occurs, the index is pushed in an errors array and the value.to_s is just returned by the mapping block.

When the iteration is done, the errors array is checked and if empty then no errors occurred and the results are returned to the assignment statement in the "accept" callback.
Otherwise I raise a custom ValueConversionError exception, setting it's message to a JSON string of the errors array. (Ie, I'm passing a list of problem inputs back up to the "accept" callback proc, so it can deal with them and give the user an indication of what is wrong.)

The HtmlUI::InputBox#type_convert() method as I have it now:

      # Callback support method that converts the string values from HTML
      # to Ruby objects based upon the default for each input control.
      # (Called from the "accept" callback block.)
      #
      # @return [Array] The converted return values.
      #
      # @raise [ValueConversionError] with the message set to a JSON string
      # representing an array of indices for those inputs with errors. In the
      # "accept" callback's rescue clause this index array will be passed on
      # over to a JS function that will highlight the problem input fields.
      #
      def type_convert(options, values)
        errors = [] # input indices with conversion errors
        results = values.each_with_index.map { |value, index|
          default = options[:inputs][index][:default]
          begin
            # Check for a class identifier:
            if default.is_a?(Class)
              if default == Length
                value.to_l
              elsif default == Float
                value.to_f
              elsif default == Integer
                value.to_i
              elsif default == TrueClass || default == FalseClass
                value
              else # String assumed
                value.to_s
              end
            else # it's not, so "sniff" the given default type:
              case default
              when Length
                value.to_l
              when Float
                value.to_f
              when Integer
                value.to_i
              when TrueClass, FalseClass
                # We're getting true/false values from Vue, no need to post-process.
                value
              else
                value.to_s
              end
            end
          rescue => err  # likely an ArgumentError from value#to_l
            # Push the input index into the errors array:
            errors << index
            value.to_s
          end
        }
        return results if errors.empty?
        # Otherwise raise an exception to mark the problem inputs. Set the
        # exception message to a JSON string representing the index array:
        fail(ValueConversionError, errors.to_json)
      end

... see next post for how the "accept" callback deals with errors ...

@DanRathbun
Copy link
Contributor Author

DanRathbun commented Oct 1, 2021

The "accept" callback

With in the #prompt() method I have modified the "accept" callback proc to handle any ValueConversionError exceptions raised when calling #type_convert():

        window.add_action_callback('accept') { |action_context, values|
          if $VERBOSE || Sketchup.debug_mode?
            puts "HtmlUI::InputBox - callback 'accept':\n  "<<values.inspect
          end
          begin
            results = type_convert(@options, values)
          rescue ValueConversionError => err
            indices = err.message # array of indices in JSON string
            window.execute_script(%[app.markProblemInputs('#{indices}');])
            UI.beep
          else
            window.close
          end
        }

So if the user flubs a Length input string, they'll hear a OS boing sound (if their speakers are on,) and the problem inputs will be highlighted.


A new CSS rule to mark problem inputs

I have added a new CSS class rule to "html/inputbox.html" in the <style> element of the document <head>:

    .bad-value, .bad-value:focus {
      background-color: #ffdcdc;
    }

This is light pastel pink color.

Adding an ID to the <form> element

It is best to start a class search from the closest root element when looking for elements by class name. So I inserted an id attribute into the app division's child <form> element thus:

    <form id="input-form" class="form-horizontal">

This way we don't search through buttons, etc. Only child elements of the <form>.

Moving the VueJS script to an external .js file

Note that I needed to move the JS into it's own "script/inputbox.js" file so it properly lexes in Notepad++.
Once I did so, the <script> at the bottom of the <body> below the app form elements was changed to:

  <script src="../script/inputbox.js"></script>

The VueJS modifications to mark problem inputs

In the now separate "/script/inputbox.js" file ...

... in the Vue app object's methods, I add 2 new functions. One to clear all inputs, the other to mark problem inputs.

        clearProblemInputs() {
          var form = document.getElementById('input-form');
          var errs = form.getElementsByClassName('bad-value');
          for (const input of errs) {
            input.classList.remove('bad-value');
          }
        },
        markProblemInputs(json_indices) {
          this.clearProblemInputs();
          var indices = JSON.parse(json_indices);
          for (const index of indices) {
            var obj = document.getElementById(this.controlId(index));
            obj.classList.add('bad-value');
          }
        }

I inserted them below the globalKeyup event handler, but they can be placed in proper alpha position.

~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant