Search code examples
google-apps-scriptclient-servergoogle-classroom

GAS Data from server side function not seen in successHandler function in HTML


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

Solution

  • 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