0

I have a sheet-bound Apps Script. I see my server side code (listCourses) in the log and lots of data is found and last statement is return data yet the JSON.stringify in the client side withSuccessHandler code (afterServerRepl(e)) sees null?

The following code is from an Alan Wells answer. I named it "tinyTestHtml.html".

<!DOCTYPE html>
<html>
  <head>
    <!--  tinyTestHtml.html -->
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>Choose a class</title>
    <base target="_top">
    <?!= HtmlService.createHtmlOutputFromFile('cssStyle').getContent(); ?>
  </head>
  <body>
    This is the body


    <script>
      window.onload = function() {
        console.log('hah! it ran. ');
        google.script.run
          .withSuccessHandler(afterServerRepl)
          .listCourses();
       };

       function afterServerRepl(e) {
         console.log('Server reply was successful!');
         console.log(JSON.stringify(e) );
       };

//       window.afterServerRepl = function(e) {
//         console.log('Server reply was successful!');
//         console.log(JSON.stringify(e) );
//       };
    </script>
  </body>
</html>

This HTML is displayed from a custom menu bound to a spreadsheet. the following is a fakeListCourses because you would only get output if you have courses in Google Classroom. The real code is included toward the bottom. You can see the result is the same as 2D array of names and ids.

function listCourses() {
  console.log('Begin listCourses');

  let unsrtCrsList = [
    ['Essential Ed', '277964684794'],
    ['TestEnglish', '256514192951'],
    ['D  Class', '57299927845'],
    ['AEL/ESL Classs', '250327591120'],
    ['Day Time ESL Multi-Level Classs', '171258050042'],
    ['English Levels 4/5s', '119335686849']
    ];
  console.log(JSON.stringify(unsrtCrsList));
  /*
    [["Essential Ed","277964684794"],["TestEnglish","256514192951"],["D  Class","57299927845"],["AEL/ESL Classs","250327591120"],["Day Time ESL Multi-Level Classs","171258050042"],["English Levels 4/5s","119335686849"]]
    */
  return unsrtCrsList;
}

Here is the function to create the sidebar and it is a template and does use EVALUATE.

function showTinyTestHtml() {
  console.log('Begin showTinyTestHtml');
  const ui = SpreadsheetApp.getUi();
  
  /**
   * Opens a sidebar to allow person to a single class
   */
  let template = HtmlService
      .createTemplateFromFile('tinyTestHtml');

  let htmlOutput = template.evaluate()
      .setTitle('Choose a Class')
      .setWidth(400);

  SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
      .showSidebar(htmlOutput);
  
}

This is the file "cssStyle.html"

    <style>
      /* comment */
      h1 {
        font-size: 20px;
      }
      
      h2 {
        font-size: 18px;
        font-weight: bold;
        padding: 5px; 
      }
      
      h3 {
        font-size: 16px;
        font-weight: bold;
        padding: 3px; 
      } /*
      .button {
        font: 1em serif;
        width: 50px;
      }

      button {
        background:  #ffffff;
        border:      2px solid #333333;
        border-radius: 5px;
        cursor:      pointer;
        font:        1em serif;
        font-size:   20;
        margin-left: 40px;
        width:       70px;
      }

      button:hover,
      button:focus {
        outline     : none;
        background  : #000000;
        color       : #FFFFFF;
      }
*/      
      #errMsg {
        border:  #ffffff;
        color:   red;
        font-size: 14px;
      }
      
      form  {
        font-size: 14px;
        margin: 0;  /* Center the form on the page */
        width: 450px;
      }
      
      form li + li {
        margin-top: 3px; 
      }
      
      input  {
        border: 1px solid #ffffff;
        box-sizing: border-box;
        font: 1em serif;
        width: 250px; 
      }
      
      .input {
        border: 1px solid #ffffff;
        box-sizing: border-box;
      }
      
      label {
        display: inline-block;
        font-weight: bold;
        text-align: right;
        width: 100px; 
      }
      
      p    {
        margin: 3px auto;
        padding: 3px; 
      }
      
      .parent  {
        background : #eeeeee;
        border: solid 2px;
        padding: 10px; 
      }
      
      ul {
        list-style: none;
        padding: 0;
        margin: 0;
      }
    </style>

Console window

enter image description here

Cocde from real listCourses. This will only give you data if you have courses in Google Classroom. I have included the display that it gets so you can see it is the same as my fake listCourses above.

/**
 * https://developers.google.com/classroom/quickstart/apps-script
 */
function listCourses() {
  console.log('Begin listCourses');
  const optionalArgs = {
    pageSize: 20
  };
  const response = Classroom.Courses.list(optionalArgs);
  const courses = response.courses;
  let unsrtCrsList = [];
  if ( courses               && 
       courses.length > 0    )    {
    for ( i = 0 ; i < courses.length ; i++ )    {
      let crsObj = courses[i];
      if ( courses[i].courseState === 'ACTIVE' )  {
        cName = courses[i].name;
        cId = courses[i].id;
        unsrtCrsList.push([cName, cId]);
      }
    }
    //  sort by first value ascending
    let srtCrsList =  gasArraySort(unsrtCrsList, 0, -1 );
    console.log(JSON.stringify(srtCrsList) );
    console.log('End listCourses - # course: ', srtCrsList.length );
    return srtCrsList;
  } else {
    console.log('End listCourses - no courses found.');
    return;
  }
}

/*
Begin listCourses
[["Essential Ed w Mattie","277964684794"],["LissaTestEnglish","256514192951"],["Lourdes AEL/ESL Class","250327591120"],["Lourdes Day Time ESL Multi-Level Class","171258050042"],["Lourdes's ESL Class","130831138700"],["Ms. Dee's Class","57299927845"],["2001-04-05T06:00:00.000Z","119335686849"]]
End listCourses - # course:  9
*/

This is a replacement for my muddled question at Tanaike File Explorer instead of Picker getFiles has data but not seen in importData

Please no AJAX, JQUERY, other fancy stuff. Just vanilla Google Apps Script. I am just a beginner.

            • adding array sort I am using 3/25

I have narrowed the problem down to the getClasses / getFiles logic and probably my attempt to sort the files in the returned list. Here is the array sort routine I am using.

/**
 *   https://sites.google.com/site/nnillixxsource/Notable/PivotChartsLib/ArrayLib
 *   sort ( data, columnIndex, ascOrDesc )
 *   veröffentlicht um 03.10.2013, 05:49 von nnil lixx   
 *   [ aktualisiert: 07.12.2013, 17:55 ]
 *   ArrayLib.sort = 
 *   I do not know how to hook up to this library
 */
function gasArraySort(data, columnIndex, ascOrDesc) {
    if (data.length > 0) {
        if (typeof columnIndex != "number" || columnIndex > data[0].length) {
            throw "Choose a valide column index";
        }
        var r = new Array();
        var areDates = true;
        for (var i = 0; i < data.length; i++) {
            if (data[i] != null) {
                var date = new Date(data[i][columnIndex]);
                if (isNaN(date.getYear()) && data[i][columnIndex] != "") {
                    areDates = false;
                } else {
                    if (data[i][columnIndex] != "") {
                        data[i][columnIndex] = date;
                    }
                }
                r.push(data[i]);
            }
        }
        return r.sort(function (a, b) {
            if (ascOrDesc) {
                return ((a[columnIndex] < b[columnIndex]) ? -1 : ((a[columnIndex] > b[columnIndex]) ? 1 : 0));
            }
            return ((a[columnIndex] > b[columnIndex]) ? -1 : ((a[columnIndex] < b[columnIndex]) ? 1 : 0));
        });
    } else {
        return data;

I built the fake list classes based on the JSON.stringify of my actual listClasses function. I have compared the output of sorted and original code and cannot see a difference other than the sort order. However, when I switch between the following two versions of getFiles the version with the sort shows up as null in the successHandler.

This is code anyone can run (no classes needed). Use inMimeType = 'EVERYTHING' to get the same results as the original. The new code sorts the files. The original code did not. The original code and the new code find the same number of files but the new code data is not visible to the successHandler in the HTML where the process started.

function getFiles(e, rootFolderId, inMimeType) {
  console.log('- - - - - - - - - Begin getFiles inMimeType: ', inMimeType );
  console.log(JSON.stringify(e) );

  var data = {};
  var idn = e;

  e = e == "root" ? DriveApp.getRootFolder().getId() : e;
  data[e] = {};
  data[e].keyname = DriveApp.getFolderById(e).getName();
  data[e].keyparent = idn == rootFolderId
    ? null : DriveApp.getFolderById(e).getParents().hasNext()
    ? DriveApp.getFolderById(e).getParents().next().getId() : null;
  data[e].files = [];
  var da = idn == "root" ? DriveApp.getRootFolder() : DriveApp.getFolderById(e);
  var folders = da.getFolders();
  var files = da.getFiles();
  
  while (folders.hasNext()) {
    var folder = folders.next();
    data[e].files.push({name: folder.getName(), id: folder.getId(), mimeType: "folder"});
  }

// new code
  var fileArr = [];
  while (files.hasNext()) {
    var file = files.next();
    console.log('6 file.getMimeType(): ', file.getMimeType());
//    data[e].files.push({name: file.getName(), id: file.getId(), mimeType: file.getMimeType()});
    if ( file.getMimeType() === 'folder' )    {;
      // do not save any files
    }    else if ( file.getMimeType() === inMimeType )    {
      console.log('7 push fileArr name: ', file.getName() );
      fileArr.push([file.getName(),file.getId(), file.getMimeType()]);
    } else if ( inMimeType === 'EVERYTHING' )  {
      console.log('8 push fileArr name: ', file.getName() );
      fileArr.push([file.getName(),file.getId(), file.getMimeType()]);
    } 
  }  
  console.log('     # files after files.hasNext: ', data[e].files.length );
  
  let sorted = gasArraySort(fileArr, 0, true);
  let numFiles = sorted.length;
  console.log('# after sort: ;', numFiles );
  for ( i = 0 ; i < numFiles ; i++ )  {
    console.log('pushing sorted[i][0]: ', sorted[i][0] );
    data[e].files.push({name: sorted[i][0], id: sorted[i][1], mimeType: sorted[i][2]});
  }

// original code
  // while (files.hasNext()) {
  //   var file = files.next();
  //   data[e].files.push({name: file.getName(), id: file.getId(), mimeType: file.getMimeType()});
  // }
  
  console.log('     # files after both while loops: ', data[e].files.length );
  console.log('end getFiles - ', JSON.stringify(data) );
  return data;
}

Here is the equivalent good and bad listClasses code and the log

Good code enabled

function listCourses() {
  console.log('Begin listCourses');
  const optionalArgs = {
    pageSize: 20
  };
  const response = Classroom.Courses.list(optionalArgs);
  const courses = response.courses;
  
  let unsrtCrsList = [], srtCrsList = [];
  console.log('# courses: ', courses.length );
  //code without sort
  if ( courses               && 
      courses.length > 0    )    {
    for ( i = 0 ; i < courses.length ; i++ )    {
      let crsObj = courses[i];
      if ( courses[i].courseState === 'ACTIVE' )  {
        cName = courses[i].name;
        cId = courses[i].id;
        console.log('pushing name: ', cName );
        srtCrsList.push([cName, cId]);
      }
    }
    console.log(JSON.stringify(srtCrsList) );
    return srtCrsList;
  
  // code with sort
//  if ( courses               && 
//       courses.length > 0    )    {
//    for ( i = 0 ; i < courses.length ; i++ )    {
//      let crsObj = courses[i];
//      if ( courses[i].courseState === 'ACTIVE' )  {
//        cName = courses[i].name;
//        cId = courses[i].id;
//        unsrtCrsList.push([cName, cId]);
//      }
//    }
//    //  sort by first value ascending
//    srtCrsList =  gasArraySort(unsrtCrsList, 0, -1 );
//    console.log(JSON.stringify(srtCrsList) );
//    console.log('End listCourses - # course: ', srtCrsList.length );
//    return srtCrsList;

  } else {
    console.log(JSON.stringify(srtCrsList) );
    console.log('End listCourses - no courses found.');
    return srtCrsList;
  } 
}

Good code log entry

Mar 25, 2021, 3:47:08 PM    Debug   Begin listCourses
Mar 25, 2021, 3:47:08 PM    Debug   # courses:  9
Mar 25, 2021, 3:47:08 PM    Debug   pushing name:  Essential Ed w Mattie
Mar 25, 2021, 3:47:08 PM    Debug   pushing name:  LissaTestEnglish
Mar 25, 2021, 3:47:08 PM    Debug   pushing name:  Ms. Dee's Class
Mar 25, 2021, 3:47:08 PM    Debug   pushing name:  Lourdes AEL/ESL Class
Mar 25, 2021, 3:47:08 PM    Debug   pushing name:  Lourdes Day Time ESL Multi-Level Class
Mar 25, 2021, 3:47:08 PM    Debug   pushing name:  Lourdes's ESL Class
Mar 25, 2021, 3:47:08 PM    Debug   pushing name:  English with Lissa Levels 4/5
Mar 25, 2021, 3:47:08 PM    Debug   [["Essential Ed w Mattie","277964684794"],["LissaTestEnglish","256514192951"],["Ms. Dee's Class","57299927845"],["Lourdes AEL/ESL Class","250327591120"],["Lourdes Day Time ESL Multi-Level Class","171258050042"],["Lourdes's ESL Class","130831138700"],["English with Lissa Levels 4/5","119335686849"]]

Bad code enabled

/**
 * https://developers.google.com/classroom/quickstart/apps-script
 */
function listCourses() {
  console.log('Begin listCourses');
  const optionalArgs = {
    pageSize: 20
  };
  const response = Classroom.Courses.list(optionalArgs);
  const courses = response.courses;
  
  let unsrtCrsList = [], srtCrsList = [];
  console.log('# courses: ', courses.length );
  //code without sort
//  if ( courses               && 
//      courses.length > 0    )    {
//    for ( i = 0 ; i < courses.length ; i++ )    {
//      let crsObj = courses[i];
//      if ( courses[i].courseState === 'ACTIVE' )  {
//        cName = courses[i].name;
//        cId = courses[i].id;
//        console.log('pushing name: ', cName );
//        srtCrsList.push([cName, cId]);
//      }
//    }
//    console.log(JSON.stringify(srtCrsList) );
//    return srtCrsList;
  
  // code with sort
  if ( courses               && 
       courses.length > 0    )    {
    for ( i = 0 ; i < courses.length ; i++ )    {
      let crsObj = courses[i];
      if ( courses[i].courseState === 'ACTIVE' )  {
        cName = courses[i].name;
        cId = courses[i].id;
        unsrtCrsList.push([cName, cId]);
      }
    }
    //  sort by first value ascending
    srtCrsList =  gasArraySort(unsrtCrsList, 0, -1 );
    console.log(JSON.stringify(srtCrsList) );
    console.log('End listCourses - # course: ', srtCrsList.length );
    return srtCrsList;
  } else {
    console.log(JSON.stringify(srtCrsList) );
    console.log('End listCourses - no courses found.');
    return srtCrsList;
  }
}

log for bad entry

Mar 25, 2021, 3:42:32 PM    Debug   Begin listCourses
Mar 25, 2021, 3:42:32 PM    Debug   # courses:  9
Mar 25, 2021, 3:42:32 PM    Debug   [["Essential Ed w Mattie","277964684794"],["LissaTestEnglish","256514192951"],["Lourdes AEL/ESL Class","250327591120"],["Lourdes Day Time ESL Multi-Level Class","171258050042"],["Lourdes's ESL Class","130831138700"],["Ms. Dee's Class","57299927845"],["2001-04-05T06:00:00.000Z","119335686849"]]
Mar 25, 2021, 3:42:32 PM    Debug   End listCourses - # course:  7
            • end 3/25 update
aNewb
  • 188
  • 1
  • 12
  • Please provide a sample of listcourses() command so that we can produce the error. Note this is the basis for requiring OPs to provide [mcve] – Cooper Mar 14 '21 at 15:35
  • I don't think that this command `!= HtmlService.createHtmlOutputFromFile('cssStyle').getContent(); ?>` will work I think it needs the evaluate() method. But admittedly I don't use templated html very much so I could be wrong. – Cooper Mar 14 '21 at 15:40
  • added simulation of listCourses (removing irrelevant details) and show HTML code. If you want the real listCourses I can supply but the JSON.stringify has the same structure [['name', 'id'],[...]] – aNewb Mar 15 '21 at 00:00
  • Please add a [mcve] (the `'cssStyle0'` file is missing) also add textually any error message show in the executions page and the web browser console. – Rubén Mar 15 '21 at 03:02
  • added css and console window displays – aNewb Mar 15 '21 at 15:29
  • Did you try evaluating the template as Cooper suggested in the comments? – Aerials Mar 17 '21 at 16:52
  • @Aerials if you look at my show modal dialog there is an evaluate. Are you and Cooper talking about something different? Added on March 15 – aNewb Mar 18 '21 at 15:10
  • @Cooper you and Aerials both pointed to 'evaluate' which I was doing. I posted the show modalDialog on March 15. Are you talking about something else I am not doing? If you have anytime I would appreciate your help. – aNewb Mar 23 '21 at 14:23
  • A lot of us probably don't use google classroom and perhaps we just don't want to load it so I added classroom to your tags perhaps someone there will be interested in giving you additional help. – Cooper Mar 23 '21 at 14:50
  • @Cooper I added the a dummy version returning the same results as listCourses so testing does not require a classroom. The problem is the same. A server side routine has lots of data but the successHandler sees null. – aNewb Mar 24 '21 at 14:10
  • I can get the results though but I deployed the script as a webApp, instead of sheet container-bound if that's what you have. – Aerials Mar 24 '21 at 15:22
  • @Aerials I guess I will have to learn what a webapp is, why they are needed, what they can and cannot do, how they are shared, changed, created... I have yet to find that beginners explanation. Of course, I don't understand why that would make a difference and I dislike the complication this adds (in my mind). – aNewb Mar 24 '21 at 16:48
  • @aNewb You don't need to, but it will help you in the long run. Anyways. I just used a web app because I started your script in a standalone project and then realized you were using a container-bound one and I didn't feel like changing it. Now I just ran your code "as is" from a container bound script and got the JSON string in the JS console of the sidebar in the sheet. So I don't know what the issue you are having is as I can't reproduce it. – Aerials Mar 25 '21 at 08:45
  • @Aerials - Thank you for sticking with me. I have narrowed the problem down and added a good and bad version to the problem statement above. – aNewb Mar 25 '21 at 17:40
  • I found my error. When I sorted the files I put the entries into arrays instead of objects. The data going to the importData function was half an array of objects and half an array of arrays. Someone with more points please close this and thanks everyone. – aNewb Mar 27 '21 at 17:44

1 Answers1

1

If I deploy a web app with your code as follows:

function doGet() {
  console.log('Begin showTinyTestHtml');
  /**
 * Opens a sidebar to allow person to a single class
 */
  let template = HtmlService
    .createTemplateFromFile('tinyTestHtml');

  let htmlOutput = template.evaluate()
    .setTitle('Choose a Class')
    .setWidth(400);

  return htmlOutput;
}

When I visit the web app URL I read the console and there is the JSON string as seen in the next image: enter image description here

UPDATE:

If I use your script "as is" and run the function showTinyTestHtml from the editor I also get the expected result. See enter image description here

Aerials
  • 4,231
  • 1
  • 16
  • 20
  • 1
    I am accepting this as an answer because given the information I had shared it is true. I subsequently discovered my error in creating the data that went into the successHandler. I am surprised the data did not go in as a malformed mess but was perceived as null but once formatted correctly all was well as you proved. Thank you very much for spending so much time trying to help me. – aNewb Mar 29 '21 at 16:46