HTML How to delete a line with a text field and button when the button is clicked

Been playing with this all day, in need of a direction please. I am trying to add and remove items from a dynamic list when a button is clicked next to a field. Total newb with html js & css, this is day 3 so any pointers would be appreciated and well received. Thanks in advance for any help.



dialog = UI::HtmlDialog.new(
    {
    :dialog_title => 'dynamic form',
    :scrollable => true,
    :resizable => true,
    :width => 600,
    :height => 300,
    :left => 200,
    :top => 200,
    :min_width => 50,
    :min_height => 50,
    :max_width =>600,
    :style => UI::HtmlDialog::STYLE_DIALOG
  })

  html = "

  <style>
.button {
  border: none;
  color: black;
  padding: 3px 8px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
}
  </style>
  
  <!DOCTYPE html>
  <html>
  <head>
    <title>Dynamic Fields</title>
  </head>
  
  <body>
    <h1>DC Attributes List &emsp;<button class='button' onclick='add()'>Add</button><button class='button' onclick='remove()'>Remove</button></h1>

    <div class='container'>
      <form action='' method='POST'>
        <div id='dc_att_list'>
        </div>
      <input name='submit' type='Submit' value='Submit'>
      </form>
    </div>
    
  <script>
    var dc_att_list = document.getElementById('dc_att_list');
  
    function add(){
      
      //add the dc attribute feild
      var input_tags = dc_att_list.getElementsByTagName('input');
      var att_no = (input_tags.length/2) + 1;
      var new_dc_att = document.createElement('input');
      new_dc_att.setAttribute('type','text');
      new_dc_att.setAttribute('name','att_text');
      new_dc_att.setAttribute('class','text');
      new_dc_att.setAttribute('value',att_no);
      new_dc_att.setAttribute('size',50);
      dc_att_list.appendChild(new_dc_att);
      
      //add the remove button
      var new_att_remove = document.createElement('input');
      new_att_remove.setAttribute('name','remove');
      new_att_remove.setAttribute('type','button');
      new_att_remove.setAttribute('class','button');
      new_att_remove.setAttribute('onclick','remove()');
      new_att_remove.setAttribute('value','Remove');
          // set id to get line num somehow?
      new_att_remove.setAttribute('id','variable'); 
          //set a variable with line num for removal of clicked line?
      new_att_remove.addEventListener('click', function handleClick() {
      var line_id = line_num; 
      });
      dc_att_list.appendChild(new_att_remove);
    }
    
        //how do I determine which line the button being clicked is on
        //to remove that line from the list and not the last?
    
    function remove(){
      var input_tags = dc_att_list.getElementsByTagName('input');
      if(input_tags.length > 0) {
        dc_att_list.removeChild(input_tags[(input_tags.length - 2)]);
        dc_att_list.removeChild(input_tags[(input_tags.length)-1]);
      }
    }
    
  </script>
  </body>
  </html>

  "
  
  dialog.set_html(html)
  dialog.show

You already have the variable att_no, so use it. Assign it as the ID to the input field, and then pass it

      new_att_remove.setAttribute('onclick','remove("${att_no}")');

OR

      new_att_remove.setAttribute('onclick','remove(att_no.toString())');

… and make the remove() function accept a string ID argument.
Within the function, find the input to delete using getElementsById(id)[0].


Notice how Ruby String interpolation uses #{ ... }, but JavaScript uses ${ ... }.
There is a difference. Ruby requies a double quoted String for interpolation, but JS does not.

2 Likes

Hey Dan, speedy with the help as usual. FYI trying/testing all this in Ruby Console+.

Yes this is what I was attempting to do (a bunch of different ways lol). I am getting an error at the $:

Screen Shot 2023-12-06 at 6.47.02 PM

Is it because of Ruby Console+? better way to test with HTML?

Why double quotes here then?

In the reading I am doing I am being told the [0] is associated with jQuery?

Attempt #127 :roll_eyes:

    function add(){
      
      //add the dc attribute feild
      var input_tags = dc_att_list.getElementsByTagName('input');
      var att_no = (input_tags.length/2) + 1;
      var new_dc_att = document.createElement('input');
      new_dc_att.setAttribute('id',att_no);
      new_dc_att.setAttribute('type','text');
      new_dc_att.setAttribute('name','att_text');
      new_dc_att.setAttribute('class','text');
      new_dc_att.setAttribute('value',att_no);
      new_dc_att.setAttribute('size',50);
      dc_att_list.appendChild(new_dc_att);
      
      //add the remove button
      var new_att_remove = document.createElement('input');
      new_att_remove.setAttribute('name','remove');
      new_att_remove.setAttribute('type','button');
      new_att_remove.setAttribute('class','button');
      new_att_remove.setAttribute('onclick','clear("$(att_no)")');
      new_att_remove.setAttribute('value','Clear');
      new_att_remove.setAttribute('id',att_no); 

      dc_att_list.appendChild(new_att_remove);
    }
    
        //how do I determine which line the button being clicked is on
        //to remove that line from the list and not the last?
    
    function clear(att_no){
      var input_tags = dc_att_list.getElementsByTagName('input');
      var to_del = dc_att_list.getElementsById(att_no);
      var to_del2 = dc_att_list.getElementsById(att_no - 1);
      if(input_tags.length > 0) {
        to_del.remove();
        to_del2.remove();
      }
    }

Could be. I have never used the browser-based console extensions as I believe they are more trouble than they’re worth.

What is clear is:

(a) that the text is being evaluated by Ruby instead of Javascript. Sorry, but I had not noticed that you were actually using a Ruby double-quoted string literal for your HTML text, ie …

  html = "
    <!-- HTML code -->
  "

This is not a good idea unless you want Ruby to do some string interpolation / replacement(s).
This is what is causing the SyntaxError. It would be better for you to use a single quoted string, as …

  html = %q[
    <!-- HTML code -->
  ]

REF: Non-Interpolable String Literals - Ruby docs

  • NOTE: With % (aka %Q,) and %q string literals you can choose any delimiters you wish. (I often use square brackets because it is unlikley that they will be used in HTML code.)

(b) that you misread what I wrote. JS string interpolation uses $ and CURLY BRACES, not parenthesis.

Because your JS function argument is wrapped in a single quote. (But again, I had not noticed you were wrapping the whole HTML literal in double quotes.)

The beauty of usings %q or just % strings is that you can embed both single and double quotes within them without causing the Ruby interpreter to puke out SyntaxErrors.

1 Like

The JS functions getElementsById and getElementsByTagName both return array. (I think!)

Also, ID should be a string, so as I showed try to convert att_no to a string. ie: att_no.toString()

BUT … before you can get the JS to work, you need to have Ruby correctly read the HTML string which it is not doing (because of the double quote wrapping.)

1 Like

(Running out to the store. Will be AFK for several hours.)

1 Like

lol was pulling my last couple hairs out. OK thanks will continue to smash away, armed with new info.

Sorry. Incorrect. It is getElementById (singular) and yes it returns a single element or null.
(So, the [0] was also incorrect in the above discussion.)

Okay, now AFK.

1 Like

Before Dan had posted, I gave an incorrect suggestion (deleted above). But I had also separated out the html because I saw you were using html in a way that I tried at first (I think we may have found the same thread on the forum to use as a template). It was a battle and I determined that separating out the html and javascript from the ruby was easier for a beginner to understand and use. And is seems like ‘best practice’.

So even though the code has mistakes (especially the commented classes, #Maybe something like this?), I’d like to share it because it may help you to create a working template for Html Dialogs (and then incorporate into something like Dan’s Multi-File template).
Another reason is that you may run into a problem (with set_file vs. set_html) if you are going to export .csv files. But we’ll see on that.

Anyway, I hope I’m not missing the point.

So as a non-functional starter stub:

The main ruby
module IDK_Programming
    module Ultimate_Toolbar
        
        @dialog_instance = nil

        def self.show_dialog
  
            dialog = UI::HtmlDialog.new(
            {
                :dialog_title => "Ultimate Toolbar!",
                :preferences_key => "com.example.Ultimate Toolbar",
                :scrollable => true,
                :width => 200,
                :height => 400,
                :left => 200,
                :top => 100,
                :resizable => true,
                :style => UI::HtmlDialog::STYLE_DIALOG
            }
            )

            # Dialog actioncallbacks
  
            # Store the dialog instance for later use
            @dialog_instance = dialog

            def self.dialog_instance
                @dialog_instance
            end
  
            # Add the toolbar button to launch the dialog
            toolbar = UI::Toolbar.new("Ultimate Toolbar!")
            cmd = UI::Command.new("Toolbar") {
            Ultimate_Toolbar.show_dialog

            @selection_observer ||= MySelectionObserver.new
            Sketchup.active_model.selection.add_observer(@selection_observer)

            # Set the HTML file to be displayed in the dialog
            dialog.set_file(File.join(File.dirname(__FILE__), 'html', 'UltimateHtml.html'))
    
            # Show the dialog
            dialog.show
        }

        #Maybe something like this?
        class MyToolsObserver < Sketchup::ToolsObserver
            def onLButtonUp(flags, x, y, view)
                # Get the current selection
                selection = Sketchup.active_model.selection

                # Check if any entities were selected
                if selection.empty?
                    Sketchup.active_model.selection.clear
                end
            end
        end

        #Maybe something like this?
        class MySelectionObserver < Sketchup::SelectionObserver
            def onSelectionBulkChange(selection)
                #.show_selected_entities(selection,???)
            end
        end

        cmd.small_icon = File.join(File.dirname(__FILE__), 'icons', 'Ultimate Toolbar.png') 
        cmd.large_icon = cmd.small_icon
        cmd.tooltip = "Ultimate Toolbar!"
        cmd.status_bar_text = "Open Ultimate Toolbar!"
        toolbar = toolbar.add_item(cmd)
        toolbar.show
    end
end
Html and javascript (with src stub to move js out)
<!DOCTYPE html>
<html>
<head>

    <title>Ultimate Toolbar</title>
    <link rel="stylesheet" type="text/css" href="style.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="../js/UltimateToolbarJS.js"></script> use if moving js into separate file*/

<script>

// Javascript functions

    var dc_att_list = document.getElementById('dc_att_list');
  
    function add(){
      
      //add the dc attribute field
      var input_tags = dc_att_list.getElementsByTagName('input');
      var att_no = (input_tags.length/2) + 1;
      var new_dc_att = document.createElement('input');
      new_dc_att.setAttribute('type','text');
      new_dc_att.setAttribute('name','att_text');
      new_dc_att.setAttribute('class','text');
      new_dc_att.setAttribute('value',att_no);
      new_dc_att.setAttribute('size',50);
      dc_att_list.appendChild(new_dc_att);
      
      //add the remove button
      var new_att_remove = document.createElement('input');
      new_att_remove.setAttribute('name','remove');
      new_att_remove.setAttribute('type','button');
      new_att_remove.setAttribute('class','button');
      new_att_remove.setAttribute('onclick','remove()');
      new_att_remove.setAttribute('value','Remove');
          // set id to get line num somehow?
      new_att_remove.setAttribute('id','variable'); 
          //set a variable with line num for removal of clicked line?
      new_att_remove.addEventListener('click', function handleClick() {
      var line_id = line_num; 
      });
      dc_att_list.appendChild(new_att_remove);
    }

    //how do I determine which line the button being clicked is on
    //to remove that line from the list and not the last?
    
    function remove(){
      var input_tags = dc_att_list.getElementsByTagName('input');
      if(input_tags.length > 0) {
        dc_att_list.removeChild(input_tags[(input_tags.length - 2)]);
        dc_att_list.removeChild(input_tags[(input_tags.length)-1]);
      }
    }

</script>
</head>
<body>
     
    <h1>DC Attributes List &emsp;<button class='button' onclick='add()'>Add</button><button class='button' onclick='remove()'>Remove</button></h1>

    <div class='container'>
      <form action='' method='POST'>
        <div id='dc_att_list'>
        </div>
      <input name='submit' type='Submit' value='Submit'>
      </form>
    </div>

</body>
</html>

Does this mean that you use Web Dialogs? I stayed away from them after I saw that they are deprecated. Does this imply you prefer the simplicity of Prompts? Is there something else that is a better way?

1 Like

Hey Dan. So my goal here is to pass an array of DC attributes from ruby to the dialog to fill in the default values. Then, after whatever changes, send the updated array of DC attributes back to ruby to continue executing the command.

So I think this is what I want to do…?

Should I go back to trying to pass an array in and out with JSON?

Just finished my paid work day and jumping back in.

Yes, I agree. Mainly because my editor (Notepad++) can better lex and color highlight the code when the HTML, Ruby, JS and CSS are in separate files.

As I tried to show in the linked topic thread, I often load the HTML and JS files directly into Ruby strings.
Then if I have %{hashkey} replacement variables in either (or both) of these files, I then use the Ruby String#% method to “stuff” the values from a Ruby hash into the HTML and/or JS strings.

2 Likes

No and No. What I mean is that I use the “plain Jane” native Ruby console. (The browser-dialog console extensions can create goofy problems that mislead newb coders.)

Your choice is to put the HTML in a separate .html file as James suggests OR use %q single quoted string if you must have the HTML inside the Ruby as a literal string object.

Again, your use of a double-quoted string html = ", coupled with erroneously using $(...) instead of ${...} (curly braces) for JS interpolation, is what was giving the SyntaxError when Ruby tried to interpolate the html string. There was no Ruby interpolation happening, so a double quoted Ruby string was not necessary.

General rule: Speed up Ruby interpreting your files by only using double quoted strings when using #{...} interpolation. Most especially with large strings like a block of HTML or CSS or JavaScript.

1 Like

I myself would not use an array. JSON is a string literal notation that describes a JavaScript Object.

The equivalent on the Ruby side is a Hash.

As I mentioned in the linked topic, you can easily go from AttributeDictionary to Hash because AttributeDictionary has the Enumerable module mixed in giving dictionaries a #to_h method.

So, then the JSON library can generate the json string from a Hash. See Generating JSON from Hashes - Ruby docs
I usually “stuff” data (from a Ruby Hash) into the JavaScript as an assignment to a JS Object in the dialog. The controls in the HTML can make changes to the JS Object by using change listeners assigned to the various controls.
The final accept button (whatever it’s labeled) would simply send the JS Object back to Ruby via a callback.

Now the UI::HtmlDialog class callback can accept a JS Object from the dialog and automatically convert it into a Ruby hash. If the hash has keys that correspond to the keys of an AttributeDictionary then updating the dictionary is straightforward using an #each iterator.

1 Like

Thank you.

Root cause identified:

= me

But I did struggle and found moving the html out simplifies things. Same with javascript. When you’re in the 99/1 fail to success camp, that 1% better feels pretty good.

I’m trying to stay away from posting code, but there was some other way (something like: html = <<). I’ll leave it at that.

1 Like

I think you mean string append with numerous append lines, ie: html << '<div>Some label</div>'
I find this tedious in the extreme and not very elegant to read. A block using %q or even a HERDOC is better for large inline literal strings.

But yes, separating code into typed files also keeps the mind on the syntax of the format / language in use.

1 Like

Yep have the %q[ html ] in, error gone. Will move to separate file method, thanks for the suggestion James.

Dan, do you have a preferred primer for js to point me at? I need to do some reading I think… This felt pretty straight forward in my head, but as usual its a rabbit hole. Gotta narrow the scope or expand the knowledge, maybe both.

1 Like

The official industry docs are at Mozilla: JavaScript | MDN

I also have bookmarked a basic quick reference from Oracle: Client-Side JavaScript Reference

Also W3Schools is a handy website:
HTML Tutorial (w3schools.com)
JavaScript Tutorial (w3schools.com)

1 Like

This is not complete just working through the parts. I am trying to figure out why this will not remove my button with the name att_x.5, what am I doing wrong. I will continue with reading and attempts, any help is greatly appreciated.


dialog = UI::HtmlDialog.new(
    {
    :dialog_title => 'dynamic form',
    :scrollable => true,
    :resizable => true,
    :width => 600,
    :height => 300,
    :left => 200,
    :top => 200,
    :min_width => 50,
    :min_height => 50,
    :max_width =>600,
    :style => UI::HtmlDialog::STYLE_DIALOG
  })

  html = %[
  
  <style>
  .button {
    border: 1px solid black;
    color: black;
    padding: 3px 8px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 16px;
    margin: 4px 2px;
    cursor: pointer;
  }
  </style>
  
  <!DOCTYPE html>
  <html>
  <head>
    <title>Dynamic Fields</title>
  </head>
  
  <body>
    <h1>DC Attributes List &emsp;<button class='button' onclick='add()'>Add</button>      </h1>

    <div class='container'>
      <form action='' method='POST'>
        <div id='dc_att_list'>
        
        </div>
      <input class='button' name='submit' type='Submit' value='Save'>
      </form>
    </div>
    
  <script>
    var dc_att_list = document.getElementById('dc_att_list');
  
    function add(){
      
      //add the dc attribute feild
      var input_tags = dc_att_list.getElementsByTagName('input');
      var att_no = (input_tags.length/2) + 1;
      var new_dc_att = document.createElement('input');
      new_dc_att.setAttribute('type','text');
      new_dc_att.setAttribute('name','att_text');
      new_dc_att.setAttribute('id','att_' + att_no);
      new_dc_att.setAttribute('class','text');
      new_dc_att.setAttribute('value',new_dc_att.id);
      new_dc_att.setAttribute('size',50);
      dc_att_list.appendChild(new_dc_att);
      
      //add the remove button
      var new_att_remove = document.createElement('input');
      var att_nam = 'att_' + (att_no + 0.5);
      new_att_remove.setAttribute('name','clear');
      new_att_remove.setAttribute('type','button');
      new_att_remove.setAttribute('class','button');
      new_att_remove.setAttribute('onclick','clear_child(att_nam)'); 
                                          //this should remove the named button only I think..?
      new_att_remove.setAttribute('id',att_nam); 
      new_att_remove.setAttribute('value','remove ' + att_nam);

      dc_att_list.appendChild(new_att_remove);
    }
    
    function clear_child(att_nam){
      var input_tags = dc_att_list.getElementsByTagName('input');
      if(input_tags.length > 0) {
        var del1 = getElementById(att_nam);
        dc_att_list.removeChild(del1); //this should remove the button only no..?
      }
    }
    
  </script>
  </body>
  </html>
  
  ]  
  
dialog.set_html(html)
dialog.show

Trying to just get the ElementById() and remove it, what am I not seeing?


    function add(){
      
      //add the dc attribute feild
      var input_tags = dc_att_list.getElementsByTagName('input');
      var att_no = (input_tags.length/2) + 1;
      var new_dc_att = document.createElement('input');
      new_dc_att.setAttribute('type','text');
      new_dc_att.setAttribute('name','att_text');
      new_dc_att.setAttribute('id','att_' + att_no);
      new_dc_att.setAttribute('class','text');
      new_dc_att.setAttribute('value',new_dc_att.id);
      new_dc_att.setAttribute('size',50);
      dc_att_list.appendChild(new_dc_att);
      
      //add the remove button
      var new_att_remove = document.createElement('input');
      var att_nam = 'att_' + (att_no + 0.5);
      new_att_remove.setAttribute('name','clear');
      new_att_remove.setAttribute('type','button');
      new_att_remove.setAttribute('class','button');
      new_att_remove.setAttribute('onclick','clear_child(att_nam)'); 
                                          //this should remove the button only..?
      new_att_remove.setAttribute('id',att_nam); 
      new_att_remove.setAttribute('value','remove ' + att_nam);

      dc_att_list.appendChild(new_att_remove);
    }
    function clear_child(att_nam){
      var input_tags = dc_att_list.getElementsByTagName('input');
      var todel = document.getElementsById(att_nam);
      if(input_tags.length > 0) {
        todel.remove()
        //dc_att_list.removeChild('att_3.5');
        //dc_att_list.removeChild(input_tags[(input_tags.length)-1]);
      }
    }

Replying to the post before last …

(1) You are still using a double-quoted string, Ie %[ is the same as %Q[ which is double quoted.
Use %q[ (lowercase) for single quoted string.

(2) Guessing …

      var del1 = getElementById(att_nam);

… there is no receiver which you are calling getElementById() upon.
Is there a default in JavaScript? Ie, does it default to the document head ? … or should you be calling:

      var del1 = document.getElementById(att_nam);

You could output the value of del1 to the JS console to see if it is what you think it should be.
Ie, open the CEF Dev Tools via a right-click on the dialog and switch to the console.

      console.log('del1 value: ${del1}')

Please do not mark topics as solved if you are going to continue to want to post questions and receive answers in the thread.