How to pass a parameter to html?

theworldismyoyster picture theworldismyoyster · May 4, 2015 · Viewed 24k times · Source

I have a script that uses the file picker but I need to pass a specific parameter which is called userId and is kept as a global variable in the calling script. As the calls are asynchronous it seems I cannot access this parameter. Is there a way to access the parameter from the html file or pass this parameter to the html?

I might be mixing templated html and non templated.

Here is the calling code (initiated through a menu item in a spreadsheet):

function syncStudentsFile(userId, ss) {
  scriptUser_(userId);  // save userId
  Logger.log('SRSConnect : syncStudentsFile : userId:'+userId);  // userId is correct here
  var ss = SpreadsheetApp.getActiveSpreadsheet();  
  var html = HtmlService.createHtmlOutputFromFile('PickerSync.html')
    .setWidth(600).setHeight(425);
  SpreadsheetApp.getUi().showModalDialog(html, 'Select a file');
}

function scriptUser_(userId) {
  if (userId !== undefined)
    sUserId = userId; // Global variable
  try { return sUserId; } catch (e) { return undefined; }
}

function getOAuthToken() {  // used by Picker
  DriveApp.getRootFolder();
  return ScriptApp.getOAuthToken();
}

Here is the html picker file:

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">

<script type="text/javascript">
  var DEVELOPER_KEY = '..............';
  var DIALOG_DIMENSIONS = {width: 600, height: 425};
  var pickerApiLoaded = false;

  /**
   * Loads the Google Picker API.
   */
  gapi.load('picker', {'callback': function() {
    pickerApiLoaded = true;
  }});

  /**
   * Gets the user's access token from the server-side script so that
   * it can be passed to Picker. This technique keeps Picker from needing to
   * show its own authorization dialog, but is only possible if the OAuth scope
   * that Picker needs is available in Apps Script. Otherwise, your Picker code
   * will need to declare its own OAuth scopes.
   */
  function getOAuthToken() {
    google.script.run.withSuccessHandler(createPicker)
        .withFailureHandler(showError).getOAuthToken();
  }

  /**
   * Creates a Picker that can access the user's spreadsheets. This function
   * uses advanced options to hide the Picker's left navigation panel and
   * default title bar.
   *
   * @param {string} token An OAuth 2.0 access token that lets Picker access the
   *     file type specified in the addView call.
   */
  function createPicker(token) {
    if (pickerApiLoaded && token) {
      var uploadView = new google.picker.DocsUploadView();
      var picker = new google.picker.PickerBuilder()
          // Instruct Picker to display only spreadsheets in Drive. For other
          // views, see https://developers.google.com/picker/docs/#otherviews
          .addView(google.picker.ViewId.DOCS)
          .addView(google.picker.ViewId.RECENTLY_PICKED)
          .addView(uploadView)
          .hideTitleBar()
          .setOAuthToken(token)
          .setDeveloperKey(DEVELOPER_KEY)
          .setCallback(pickerCallback)
          // Instruct Picker to fill the dialog, minus 2 pixels for the border.
          .setSize(DIALOG_DIMENSIONS.width - 2,
              DIALOG_DIMENSIONS.height - 2)
          .build();
      picker.setVisible(true);
    } else {
      showError('Unable to load the file picker.');
    }
  }

  /**
   * A callback function that extracts the chosen document's metadata from the
   * response object. For details on the response object, see
   * https://developers.google.com/picker/docs/result
   *
   * @param {object} data The response object.
   */
  function pickerCallback(data) {
    var action = data[google.picker.Response.ACTION];
    if (action == google.picker.Action.PICKED) {
      var doc = data[google.picker.Response.DOCUMENTS][0];
      var id = doc[google.picker.Document.ID];
      google.script.host.close();
      // --------------> user global parameter sUserId set earlier
      google.script.run.PickerSyncFile(sUserId, id);
    } else if (action == google.picker.Action.CANCEL) {
      google.script.host.close();
    }
  }

  /**
   * Displays an error message within the #result element.
   *
   * @param {string} message The error message to display.
   */
  function showError(message) {
    document.getElementById('result').innerHTML = 'Error: ' + message;
  }
</script>

<div>
  <script>getOAuthToken()</script>
  <p id='result'></p>
  <input type="button" value="Close" onclick="google.script.host.close()" />
</div>

Here is the picker code:

function pickerSyncFile(userId, id) {
  Logger.log('userId:'+userId);  // BUG: it is null
  Logger.log('id:'+id);  // id returned well from picker

  // rest of code here but userId was is incorrect
}

Answer

Zig Mandel picture Zig Mandel · Jul 11, 2016

The safest way is to pass the needed data to the HTML directly. If you use properties or cache service it can get complex or fail under multiple simultaneous users.
There are many techniques to pass an initial object from the server (.gs) to the client (.html).

Using HtmlTemplate, you may do:
//.gs file

function doGet() {
    var htmlTemplate = HtmlService.createTemplateFromFile('template-client');
    htmlTemplate.dataFromServerTemplate = { first: "hello", last: "world" };
    var htmlOutput = htmlTemplate.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME)
        .setTitle('sample');
    return htmlOutput;
}

and in your template-client.html file:

<!DOCTYPE html>

<script>
    var data = <?!= JSON.stringify(dataFromServerTemplate) ?>; //Stores the data directly in the javascript code
    // sample usage
    function initialize() {
        document.getElementById("myTitle").innerText = data.first + " - " + data.last;
        //or use jquery:  $("#myTitle").text(data.first + " - " + data.last);
    }
    // use onload or use jquery to call your initialization after the document loads
    window.onload = initialize;
</script>


<html>
<body>
    <H2 id="myTitle"></H2>
</body>
</html>

It is also possible to do it without using templating, by appending a hidden div to an HtmlOutput:

//.gs file:

function appendDataToHtmlOutput(data, htmlOutput, idData) {
    if (!idData)
        idData = "mydata_htmlservice";

    // data is encoded after stringifying to guarantee a safe string that will never conflict with the html.
    // downside: increases the storage size by about 30%. If that is a concern (when passing huge objects) you may use base94
    // or even base128 encoding but that requires more code and can have issues, see http://stackoverflow.com/questions/6008047/why-dont-people-use-base128
    var strAppend = "<div id='" + idData + "' style='display:none;'>" + Utilities.base64Encode(JSON.stringify(data)) + "</div>";
    return htmlOutput.append(strAppend);
}


// sample usage:
function doGet() {
    var htmlOutput = HtmlService.createHtmlOutputFromFile('html-sample')
        .setSandboxMode(HtmlService.SandboxMode.IFRAME)
        .setTitle('sample');

    // data can be any (serializable) javascript object.
    // if your data is a native value (like a single number) pass an object like {num:myNumber}
    var data = { first: "hello", last: "world" };
    // appendDataToHtmlOutput modifies the html and returns the same htmlOutput object
    return appendDataToHtmlOutput(data, htmlOutput);
}

and in your output-client.html:

<!DOCTYPE html>
<script>
    /**
    * getDataFromHtml
    *
    * Inputs
    * idData: optional. id for the data element. defaults to "mydata_htmlservice"
    *
    * Returns
    * The stored data object
    */
    function getDataFromHtml(idData) {
        if (!idData)
            idData = "mydata_htmlservice";
        var dataEncoded = document.getElementById(idData).innerHTML;
        var data = JSON.parse(atob(dataEncoded));
        return data;
    }
    // sample usage of getDataFromHtml
    function initialize() {
        var data = getDataFromHtml();
        document.getElementById("myTitle").innerText = data.first + " - " + data.last;
        //or use jquery:  $("#myTitle").text(data.first + " - " + data.last);
    }
    // use onload or use jquery to call your initialization after the document loads
    window.onload = initialize;
</script>
<html>
<body>
    <H2 id="myTitle"></H2>
</body>
</html>


Both methods are compared and better explained in this little github I made: https://github.com/zmandel/htmlService-get-set-data