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
15
  • Please provide a sample of listcourses() command so that we can produce the error. Note this is the basis for requiring OPs to provide minimal reproducible example Commented Mar 14, 2021 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. Commented Mar 14, 2021 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'],[...]] Commented Mar 15, 2021 at 0:00
  • Please add a minimal reproducible example (the 'cssStyle0' file is missing) also add textually any error message show in the executions page and the web browser console. Commented Mar 15, 2021 at 3:02
  • added css and console window displays Commented Mar 15, 2021 at 15:29

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.