-3

I have a dynamic HTML form that I generate from Google Apps Script. I expect the elements to appear in the same order that they appear in the input object, which is the same order by which I append the elements. But the elements actually appear in an unexpected order.

I followed this other post to try to sort this out but the elements still appear out of order when I run my code in GAS. The thing is, when I run my code in jsfiddle, it works as expected, i.e. the elements appear in the same order as they do in the input object. The elements just don't order as expected in GAS.

Why do the elements appear out of order in GAS but appear in order in jsfiddle? How do I resolve this in GAS using vanilla JS?

Copy of jsfiddle code:

HTML

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  </head>
      
    <div id='input_parent'></div>
  </body>  



  
    <br><input type="button" value="Submit" onClick="test()">
    
    </form>

JS

inputObj = {"first field":{"required":true,"dataType":"select","options":["first opt","second opt"]},"second field":{"required":true,"dataType":"text","options":"none"}}
        // Section  
        section = document.getElementById("input_parent");

        div = document.createElement("div");
        div.setAttribute("id", "input_child");

        section.appendChild(div); 

             

          var fields = Object.keys(inputObj);     

          Array.from(fields).forEach((arg) => { 

            // Label
            section = document.getElementById("input_parent");

            label = document.createElement("label");
            label.setAttribute("id", "label_"+arg);
            label.setAttribute("for", arg);
            label_txt = document.createTextNode(arg+":");
            label.appendChild(label_txt);

            section.appendChild(label); 
            
            
                if (inputObj[arg].dataType == "select") { 
                      // Create select element                  
                      section = document.getElementById("input_parent");
                      const select_element = document.createElement("select");
                      select_element.setAttribute("id", "select_"+arg);               
                      
                      section.appendChild(select_element);
                      
                      var options = inputObj[arg].options
                      for(let o = 0; o < options.length; o++)
                      {
                        var element = document.getElementById("select_"+arg);
                        const option = document.createElement("option");
                        var text = document.createTextNode(arg+":");
                        option.textContent = options[o];
                        option.setAttribute("value", options[o]);
                        element.appendChild(option);

                      };

                     
                                            
                  } else {

                    section = document.getElementById("input_parent");
                    input_field = document.createElement("input");
                    input_field.setAttribute("id", "input_"+arg);          
                    input_field.setAttribute("type", inputObj[arg].dataType);

                section.appendChild(input_field);

                }

              

        });

Additional info in response to @Nikko J. The following code should reproduce the results in the images below.

dynamHtmlTbrlsht.html to render form.

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <div id='input_parent'></div>
  </body>
   <br><input type="button" value="Submit" onClick="test()">
</html>


<script type='text/javascript'>
window.onload = function(){   
  google.script.run.withSuccessHandler(addElements).getElementInfo();   

  }
</script>


<script type='text/javascript'> 
function addElements(inputObj) {
// Section  
        section = document.getElementById("input_parent");

        div = document.createElement("div");
        div.setAttribute("id", "input_child");

        section.appendChild(div); 

             

          var fields = Object.keys(inputObj);     

          Array.from(fields).forEach((arg) => { 

            // Label
            section = document.getElementById("input_parent");

            label = document.createElement("label");
            label.setAttribute("id", "label_"+arg);
            label.setAttribute("for", arg);
            label_txt = document.createTextNode(arg+":");
            label.appendChild(label_txt);

            section.appendChild(label); 
            
            
                if (inputObj[arg].dataType == "select") { 
                      // Create select element                  
                      section = document.getElementById("input_parent");
                      const select_element = document.createElement("select");
                      select_element.setAttribute("id", "select_"+arg);               
                      
                      section.appendChild(select_element);
                      
                      var options = inputObj[arg].options
                      for(let o = 0; o < options.length; o++)
                      {
                        var element = document.getElementById("select_"+arg);
                        const option = document.createElement("option");
                        var text = document.createTextNode(arg+":");
                        option.textContent = options[o];
                        option.setAttribute("value", options[o]);
                        element.appendChild(option);

                      };

                     
                                            
                  } else {

                    section = document.getElementById("input_parent");
                    input_field = document.createElement("input");
                    input_field.setAttribute("id", "input_"+arg);          
                    input_field.setAttribute("type", inputObj[arg].dataType);

                section.appendChild(input_field);

                }

              

        });
}

</script>        

trblsht.gs to create input object. (Note that this is simplified for resolving the problem at hand. In reality, inputObj is generated by running a few functions that dynamically create the object and fetch options from an external source.)

function getElementInfo() {
  
  var inputObj = {"first_fild":{"required":true,"dataType":"select","options":["option 1","option 2","option 3","option 4"]},"second_field":{"required":true,"dataType":"text","options":"none"},"third_field":{"required":true,"dataType":"text","options":"none"},"fourth_field":{"required":true,"dataType":"text","options":"none"},"fifth_field":{"required":false,"dataType":"select","options":["option 1","option 2","option 3","option 4","option 5","option 6","option 7","option 8","option 9","option10"]},"sixth_field":{"required":false,"options":"none"},"seventh_field":{"required":false,"dataType":"select","options":["option 1","option 2","option 3","option 4","option 5","option 6"]}}
  

  Logger.log("inputObj: "+JSON.stringify(inputObj))
  return inputObj;
}

For completeness, the following lines are in an onEdit function that generates the form when the active cell == "troubleshoot".

function myOnEditTriggerFunc() {
    // do other stuff
    var currentRange = SpreadsheetApp.getActiveRange();
    var currentVal = 
    currentRange.getValue().toString().replace(/(^\s+|\s+$)/g,"");
    if (currentVal == "troubleshoot") {
        openHTML("dynamHtmlTbrlsht","Troubleshoot",400)
        return;
  }
  

}

openHTML() referenced in above function.

function openHTML(htmlFile,htmlTitle,height) {
      var html = HtmlService.createHtmlOutputFromFile(htmlFile)
      .setSandboxMode(HtmlService.SandboxMode.IFRAME)
      .setHeight(height);

    SpreadsheetApp.getUi() 
      .showModalDialog(html, htmlTitle);
      return;
 };

Output form showing unexpected order of elements: enter image description here

Output log showing expected order of elements: enter image description here

The jsfiddle shows the normal js.

I started to wonder if the issue is with Array.from(fields).forEach((arg) in dynamHtmlTbrlsht.html. I intentionally used Array instead of Object since Array is ordered whereas Object is not always ordered (depends on ES). Maybe there's something related to V8 runtime that's affecting this and I'm not picking up on?

user8121557
  • 149
  • 2
  • 9
  • How did you test this in apps script? Could you show us your code in apps script and screenshot of output in GAS vs in normal JS. Also see [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) – Nikko J. Dec 22 '21 at 18:40

1 Answers1

0

When I saw your script, I thought that the reason for your issue is due to that the data is a JSON object. When the document about JSON is seen, it says as follows. Ref

In JSON, they take on these forms:

An object is an unordered set of name/value pairs. An object begins with {left brace and ends with }right brace. Each name is followed by :colon and the name/value pairs are separated by ,comma.

And, in your script, the values are retrieved from the JSON object using var fields = Object.keys(inputObj). In order to confirm this using your script, when console.log(Object.keys(inputObj)) is put after the line of function addElements(inputObj) { at Javascript side and before the line of return inputObj; at Google Apps Script side, each log shows as follows.

  1. For Javascript side, it's ["fourth_field", "seventh_field", "second_field", "sixth_field", "third_field", "fifth_field", "first_fild"].

  2. For Google Apps Script side, it's ["first_fild","second_field","third_field","fourth_field","fifth_field","sixth_field","seventh_field"].

It is found that the order of keys is different between the Javascript side and Google Apps script side. I thought that this might be the reason for your issue.

If you want to use the order of ["first_fild","second_field","third_field","fourth_field","fifth_field","sixth_field","seventh_field"], how about the following modification?

Modified script 1:

In this pattern, the keys are set as an array in order.

Google Apps Script side:

From:

return inputObj;

To:

return [inputObj, ["first_fild","second_field","third_field","fourth_field","fifth_field","sixth_field","seventh_field"]];

Javascript side:

From:

function addElements(inputObj) {
// Section  
        section = document.getElementById("input_parent");

        div = document.createElement("div");
        div.setAttribute("id", "input_child");

        section.appendChild(div); 

             

          var fields = Object.keys(inputObj);  

To:

function addElements([inputObj, fields]) {
// Section  
        section = document.getElementById("input_parent");

        div = document.createElement("div");
        div.setAttribute("id", "input_child");

        section.appendChild(div); 

             

          // var fields = Object.keys(inputObj);  

Modified script 2:

In this pattern, the keys are set as Object.keys(inputObj) in order. By this, the same order can be used between the Google Apps Script side and the Javascript side.

Google Apps Script side:

From:

return inputObj;

To:

return [inputObj, Object.keys(inputObj)];

In this pattern, the modification of the Javascript side is the same as pattern 1.

Tanaike
  • 181,128
  • 11
  • 97
  • 165
  • Thanks. Mod 2 worked. You looked at the output of the GAS-side and JavaScript-side. Does the JavaScript-side that you mention refer to the JS in the HTML file? If so, how did you access the output from the JS-side via GAS? – user8121557 Dec 23 '21 at 15:45
  • @user8121557 Thank you for replying. I'm glad your issue was resolved. About your additional question 1 of `Does the JavaScript-side that you mention refer to the JS in the HTML file?`, it's yes. About `If so, how did you access the output from the JS-side via GAS?`, I have to apologize for my poor English skill. Unfortunately, I cannot understand your 2nd additional question. Can I ask you about the detail of it? – Tanaike Dec 24 '21 at 00:23