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

Got it.

This is very helpful thanks.

Roger.

Ok The clear_child function is telling me that att_nam is undefined. The goal at this point is for this button to remove itself when clicked.


    var dc_att_list = document.getElementById('dc_att_list');
  
    function add(){
      
      //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, how to make the att_nam variable stick?
      new_att_remove.setAttribute('id',att_nam); 
      new_att_remove.setAttribute('value','remove ' + att_nam);

      dc_att_list.appendChild(new_att_remove);
    }

//error is att_nam undefined when the button is clicked
    function clear_child(att_nam){
      var input_tags = dc_att_list.getElementsByTagName('input');
      var todel = document.getElementById(att_nam);
      console.log('todel value: ${todel}');
      if(input_tags.length > 0) {
        //todel.remove()

      }
    }

Well then, this statement in the add() function is not working as you intended:

      var att_nam = 'att_' + (att_no + 0.5);

I take it that you want each item in the list to have a ordinal position number.
Why not define a global (toplevel) variable for this position initially set to 0 ?
Then each time the add() function is called, you increment it by 1 ?

I don’t think so? I am trying to bite off little bits and get a basic js understanding going at the same time. Maybe there is a better order of attack?

The grand scheme:

First: Populate the dialog with a previously saved list of attributes residing in my extensions ruby environment.

Second: Adjust the list. Ultimately I want the input field and corresponding button to both be removed when the button is clicked. I think I need to put them together as a single Object (in a div maybe?) and assign it the id that I want to remove. To be clearer, I want to be able to add and remove individual lines out of order, now that I am thinking about it, I will probably want to put the add button on each line also, when clicked it would add a new next line below.

Third: Send the edited list back to my extensions Ruby environment to be saved and used later.

I imagine there are parts of this that are a but tricksy…

AFK Doggo needs to get out, back in a few.

Thanks!

Edit: lol, gotta be careful with those html brackets.

Likely wrap them both in a <div> element with a unique id.
The wrapped <input> and <button> could have the parent <div>'s id as a prefix to their ids.

1 Like

A Hash on the Ruby-side converted to a JSON string, which is inserted into the dialog’s JavaScript assigning the JS object to a object reference.

There are several ways to do this. I prefer to “stuff” it into the string of JavaScript that I’ve loaded into a Ruby variable. Then I “stuff” the JS text into the HTML text which I’ve also loaded into a Ruby string variable from a file. (I use the String#% method to do the “stuffing”.)

An onload listener that iterates the JS data object and builds the list dynamically from the object’s values as loaded.
Each input field in the list gets a listener that when changed updates the value in the master JS data object.

But you could also have a dialog “ready” callback that sends the JS object data over using dialog#execute_script then calls the JS function to populate the list.

As said before, sending a JS object to a Ruby dialog callback automatically converts it to a Ruby hash.

1 Like

OK trying to map out all this out, think I am making progress… however I am getting a get_atts not defined error. Why is it not making it into the script as an Object? Trying to mimic this thread you pointed at earlier:


att_hash = { att_1: 'dc att_1 value', 
att_2: 'dc att_2 value', 
att_3: 'dc att_3 value' }

pass_atts_in = att_hash.to_json
dialog.execute_script( %[var get_atts = JSON.parse('#{pass_atts_in}');] )
// there is a dialog attached to the top of this leaving it out for brevity.

html = %q[
  
    
    <script>

        function populate_list(){
            // run once at start
            // Iterate imported get_atts hash

            Object.keys(get_atts).forEach(function(key) {
              // key = att_1,2,etc  get_atts[att_1] = 'dc attribute string'
              console.log('Key : ' + key + ', Value : ' + get_atts[key])
              // block to build initial form
              add(key)
            })
        }


        function add(key){
      
          //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',key);
          new_dc_att.setAttribute('class','text');
          new_dc_att.setAttribute('value',get_atts(key));
          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, how to make the att_nam variable stick?
          new_att_remove.setAttribute('id',att_nam); 
          new_att_remove.setAttribute('value','remove ' + att_no);

          dc_att_list.appendChild(new_att_remove);
        }

    
    
    </script>

This pattern works to load the Ruby hash into the dialog JS as an object …

I think it was that the ready state callback on the Ruby-side was not getting called.

There also was a minor error down in the add() function where it stuffed the attribute value into the body:
You were using …

            new_dc_att.setAttribute('value',get_atts(key));

… which resulted in the JS error “get_atts is not a function”.
This is true. It is a JS object so either use square brackets or dot notation.

            new_dc_att.setAttribute('value',get_atts[key]);

… works.

Working pattern to load the attribute data (missing the edit functions)
def go

  @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
    })

  att_hash = {
    att_1: 'dc att_1 value', 
    att_2: 'dc att_2 value', 
    att_3: 'dc att_3 value'
  }

  @dialog.add_action_callback('ready') do |context, params|
    puts "Dialog callback \"ready\" (dialog static content was loaded)"
    pass_atts_in = att_hash.to_json
    @dialog.execute_script( %[var get_atts = JSON.parse('#{pass_atts_in}');] )
    @dialog.execute_script( %[populate_list();] )
  end

  html = %q[
      <!DOCTYPE html>
      <html>
      <head>
          <title>Dynamic Fields</title>

          <style>
              body {
                width: 100%;
                height: 100%;
              }
              .button {
                border: 1px solid black;
                color: black;
                padding: 3px 8px;
                display: inline-block;
                font-size: 16px;
                margin: 4px 2px;
                cursor: pointer;
              }
              .container {
                width: 100%;
              }
          </style>

          <script>
              document.addEventListener("readystatechange", function (e) {
                  if( document.readyState == 'complete' ) {
                      sketchup.ready();
                  }
              });
          </script>

      </head>
      
      <body>
        <h1>DC Attributes List &emsp;<button class='button' onclick='add()'>Add</button>      </h1>
        <br>
        <div class='container'>

            <div id='dc_att_list'>
            
            </div>
            <br>
          <input class='button' name='submit' type='Submit' value='Save'>

        </div>
        
      </body>

      <script>

          var dc_att_list = document.getElementById('dc_att_list');

          function populate_list() {
              // run once at start
              console.log('In populate_list() function ...');
   
              // Iterate the get_atts Object imported from Ruby hash:
              Object.keys(get_atts).forEach(function(key) {
                // key = att_1,2,etc  get_atts[att_1] = 'dc attribute string'
                console.log('Key : ' + key + ', Value : ' + get_atts[key])
                // block to build initial form
                add(key)
              })
          }


          function add(key) {
        
            //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',key);
            new_dc_att.setAttribute('class','text');
            new_dc_att.setAttribute('value',get_atts[key]);
            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, how to make the att_nam variable stick?
            new_att_remove.setAttribute('id',att_nam); 
            new_att_remove.setAttribute('value','remove ' + att_no);

            dc_att_list.appendChild(new_att_remove);
          }

      </script>

      </html>
  ] # end of HTML

  @dialog.set_html(html)

  @dialog.show

end # go()
1 Like

Amazing. Thanks Dan. Will keep forging ahead…

EDIT: For anyone following along I found this super helpful.

Hey :wave: So I have this working pretty well so far. However I am having trouble getting the parent id of my add button when clicked, and passing it to the insert into document body function. So a question about js: let, var and const - which when why?

            var parent_div_id = 'div0';
            //the above makes it to the insert function
            
            function set_parent_id() {
              var parent_div_id = 'div1';
              //the above does not make it to the insert function.
              console.log('parent div id = ' + parent_div_id);
            }

For getting the id I have tried using this, an event listener and a bunch of other stuff. Not sure which approach would be best here? Maybe something I have not considered yet?

The code thus far:

def go

  @dialog = UI::HtmlDialog.new(
      {
      :dialog_title => 'DC Tools - Attribute Handler',
      :scrollable => true,
      :resizable => true,
      :width => 500,
      :height => 300,
      :left => 200,
      :top => 200,
      :min_width => 235,
      :min_height => 125,
      :max_width =>430,
      :style => UI::HtmlDialog::STYLE_DIALOG
    })

  att_hash = {
    att_1: 'dc att_1 value', 
    att_2: 'dc att_2 value', 
    att_3: 'dc att_3 value'
  }

  @dialog.add_action_callback('ready') do |context, params|
    puts "Dialog callback \"ready\" (dialog static content was loaded)"
    pass_atts_in = att_hash.to_json
    @dialog.execute_script( %[var get_atts = JSON.parse('#{pass_atts_in}');] )
    @dialog.execute_script( %[populate_list();] )
  end

  html = %q[
      <!DOCTYPE html>
      <html>
      <head>
          <title>Dynamic Attributes Handler</title>

          <style>
              body {
                width: 100%;
                height: 100%;
              }
              .buttonred {
                border: none;
                color: white;
                background-color: rgb(216, 54, 54);
                padding: 3px 8px;
                display: inline-block;
                font-size: 12px;
                margin: 4px 2px;
                cursor: pointer;
              }
              .buttongreen {
                border: none;
                color: white;
                background-color: rgb(30, 123, 72);
                padding: 3px 8px;
                display: inline-block;
                font-size: 12px;
                margin: 4px 2px;
                cursor: pointer;
              }
              .container {
                width: 100%;
              }
          </style>

          <script>
              document.addEventListener("readystatechange", function (e) {
                  if( document.readyState == 'complete' ) {
                      sketchup.ready();
                  }
              });
          </script>

      </head>
      <h3 style='text-align:left'>DC Attributes List&ensp;<button class='buttongreen' onclick='load_atts()'>+</button></h3> 

      <body>

        <div class='container'>

            <div id='dc_att_list'>
            </div>

        </div>
      </body>

      <script>
            function populate_list() {
                // run once at start
                console.log('In populate_list() function ...');
    
                // Iterate the get_atts Object imported from Ruby hash:
                Object.keys(get_atts).forEach( function(key) {
                    console.log('Key : ' + key + ', Value : ' + get_atts[key]);
                    load_atts(key);
                })
            }

            var num = 0;
            var parent_div_id = 'div0';
            
            function set_parent_id() {
              var parent_div_id = 'div1';
              console.log('parent div id = ' + parent_div_id);
            }
            
            function load_atts(key) {
                console.log('parent div id = ' + parent_div_id);
                // create a new div element to hold line inputs
                num +=1;
                var div_nam = 'div' + num;
                var new_line = document.createElement('div');
                new_line.setAttribute('id',div_nam);
                console.log(new_line.id);
                
                if (key) {
                  //add the dc attribute feild
                  var load_dc_att = document.createElement('input');
                  load_dc_att.setAttribute('type','text');
                  load_dc_att.setAttribute('name','att_text');
                  load_dc_att.setAttribute('id',key);
                  load_dc_att.setAttribute('class','text');
                  load_dc_att.setAttribute('value',get_atts[key]);
                  load_dc_att.setAttribute('size',50);
                  new_line.appendChild(load_dc_att);
                } else {
                  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_');
                  new_dc_att.setAttribute('class','text');
                  new_dc_att.setAttribute('value','enter_attribute');
                  new_dc_att.setAttribute('size',50);
                  new_line.appendChild(new_dc_att);
                }
                
                //ceate and add the remove_line button
                var rmv_button = document.createElement('input');
                rmv_button.setAttribute('name','remove_line');
                rmv_button.setAttribute('type','button');
                rmv_button.setAttribute('class','buttonred');
                rmv_button.setAttribute('onclick','return this.parentNode.remove();'); 
                rmv_button.setAttribute('value','-');
                new_line.appendChild(rmv_button);

                //ceate and add the add_line button
                var add_button = document.createElement('input');
                add_button.setAttribute('name','add_line');
                add_button.setAttribute('type','button');
                add_button.setAttribute('class','buttongreen');
                add_button.setAttribute('onclick','set_parent_id(); load_atts()'); 
                add_button.setAttribute('value','+');
                new_line.appendChild(add_button);
                
                var br = document.createElement('br');
                new_line.appendChild(br);
                
                // add the newly created element and its content into the DOM
                // need to know which line I am on here
                var current_line = document.getElementById(parent_div_id);
                document.body.insertBefore(new_line, current_line);
            
            }



        </script>

        </html>
  ] # end of HTML

  @dialog.set_html(html)

  @dialog.show

end # go()

go

Edit: Hey Dan, answered myself here and have it working, on to next step.

The HTML DOM implements class inheritance so subclasses can inherit functionality (property getters and methods) from it’s ancestor superclasses.

So, read about the Node interface …

Looking at the properties that Node should pass down to Element, we see one that should really help you out here:

So, in your onClick you could hopefully do: "this.parentElement.remove();"

2 Likes

I don’t understand the problem but I use getElementById.

1 Like

Not exactly what I did but close: “return this.parentNode.remove();”

And a listener with onclick to get the parent id and add a new line above.

            function add_listener(my_button) {
              my_button.addEventListener('click', function handleClick(event) {
                parent_div_id = event.target.parentElement.id;
                new_att_line();
              });
            }


                // and at the end of the new line block
                var current_line = document.getElementById(parent_div_id);
                document.body.insertBefore(new_line, current_line);

Anyways, next is putting the edited list back into a hash to send back to Ruby.

Trying to determine which button (line) was clicked, so I could add a new line directly above it.

1 Like

Yea, but why mess with the id when you can directly get a reference to the parent element ?

:thinking: Because I am new. :rofl: :shushing_face:

Took me a minute but like this?:

  var parent_element = null
  function add_listener(my_button) {
      my_button.addEventListener('click', function handleClick(event) {
        parent_element = this.parentElement
        new_att_line();
      });
   }

Well, JS is also multiparadigm, so there will several ways to do things.

I would think that the parent element reference would be passed as a parameter to the function adding the new line.

  function add_listener(my_button) {
      my_button.addEventListener('click', function handleClick(event) {
        new_att_line(event.target.parentElement);
        // NOTE: "this" points at event.currentTarget which changes
        //   as the event bubbles up through the DOM hierarchy.
        // Whereas, event.target remains pointing at what fired the event.
      });
   }

  function new_att_line(current_line) {
    var new_line = document.createElement('div');
    // set div's attributes
    current_line.insertAdjacentElement('beforebegin', new_line)
    // add the edit control and buttons
  }

REF: Element: insertAdjacentElement() method - Web APIs | MDN (mozilla.org)

In this way there may be no need to worry about element IDs. Meaning that if you know where you are in the document node tree, you can add and delete other elements directly form where you are, rather than searching the whole document tree for a particular element by ID.

2 Likes

Yes this worked great! Got rid of some extra lines, much cleaner. Now on to hash.mapping…

EDIT: Think that I want js map not Java HashMap

So I think I want to:

  1. Get all the text fields with attributes.
    getById ‘dc_att_list’ - parent element of all dc attributes
    then getByTag ‘input’ - all input fields
    then getByType ‘text’ - all text fields, which is my data to hash

  2. Iterate to build a hash of the attributes, pulling the value attribute from the text fields.

  @dialog.add_action_callback('sendDataToRuby') { |action_context, my_hash|
    att_hash = my_hash
    puts my_hash
  }

and assign sendDataToRuby to a button in @dialog.

I think.

Well, you may not need to do this. You already have the get_atts JS Object that you sent from the Ruby hash (att_hash).

Just add an event listener to each <input> element to copy the value from the text from the <input> element into the get_atts object.

load_dc_att.addEventListener("input", (event) => {
  var att = event.target;
  var key = att.id;
  var val = att.value;
  get_atts[key] = val;
});

Then when an Apply (or Save or whatever named) button is clicked just send the get_atts JS Object back to Ruby.

REF: <input>: The Input (Form Input) element - HTML: HyperText Markup Language | MDN
(Shows also how to validate input)


Iterating a JS Object

You are using:

                // Iterate the get_atts Object imported from Ruby hash:
                Object.keys(get_atts).forEach( function(key) {
                    console.log('Key : ' + key + ', Value : ' + get_atts[key]);
                    load_atts(key);
                })

You may find the forin construct easier to understand:

                // Iterate the get_atts Object imported from Ruby hash:
                for (const key in get_atts) {
                    console.log('Key : ' + key + ', Value : ' + get_atts[key]);
                    load_atts(key);
                }

REF: for…in - JavaScript | MDN (mozilla.org)

1 Like

Quick question before I jump in here: I want the order of the attributes to be retained between dialog sessions. Does that change anything approach wise? Will they remember their positions on reload of the dialog?