How to pass a parameter to html?
Asked Answered
C

5

16

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
}
Currie answered 4/5, 2015 at 15:4 Comment(0)
C
34

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 https://mcmap.net/q/236013/-why-is-base128-not-used-closed
    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

Calends answered 11/7, 2016 at 18:45 Comment(2)
for UTF-8 charaters using this code Utilities.base64Encode(past, Utilities.Charset.UTF_8)Fireball
A great approach for me to get to know html script tag. Thank you!Retrieve
T
6

I often use HtmlService templates to push static values to the client.

index.html

<script>
 var domain = "<?=domain?>"; 
</script>

code.gs

var ui  = HtmlService.createTemplateFromFile('Sidebar');
ui.domain = domain;
return ui.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).setTitle(strings[lang][domain]);
Timmytimocracy answered 4/5, 2015 at 17:12 Comment(4)
Thanks for that but how can use template html in conjunction with my html code so it will work?Currie
You question is how to pass a parameter to html. You code shows you understand google.script.run, so I showed you the template method. Please revise and clarify your question.Timmytimocracy
I was trying yo combine the 2 nothing seem to work, hence looking for passing the parameter. .evaluate on the html (in syncStudentsFile) kept returning errors. I am not that of an expert in regard to how to work this html. Hence my question.Currie
This solution is the official way I think, but it has a problem: when the template file is too large, apps script will return "Exceeded maximum stack depth" error.Spiritless
L
3

In your code:

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

You are assigning a value to the global variable named sUserId. But, then when you try to retrieve it, nothing is there. Global variables loose their value as soon as the current instance of the code bring run is completed. Global variable don't persist their values.

You'll need to use the Properties Service to store the value. Or you could use the Cache service. If you want the value of the user id to expire after some time, use cache service.

Lunatic answered 4/5, 2015 at 17:12 Comment(1)
Cache didn't seem to work but I will give Properties Service a try... Would have been easier if a parameter could have been passed. Thanks!Currie
S
1

By Appending to the HTML File, as shown below.

within Code.gs

function showDialog() {
    var html = HtmlService.createHtmlOutputFromFile('html-sample')
                .setSandboxMode(HtmlService.SandboxMode.IFRAME)
                .setWidth(600)
                .setHeight(425);

    var data = "Hello World!";
    var strAppend = "<div id='id_for_div' style='display:none;'>" + data + "</div>";
    html.append(strAppend);

   var title = "Demo";
   SpreadsheetApp.getUi().showModalDialog(html, title); // or DocumentApp or SlidesApp or FormApp.
}

html-sample.html

<!DOCTYPE html>
<html>
   <head>
      <script>
         function setText(text) {
           var element = document.getElementById("myid");
           element.innerHTML = text;
         }

         function getDataFromHtml() {
           var id = "id_for_div";
           var dataEncoded = document.getElementById(id).innerHTML;
           setText(dataEncoded);
         }

      </script>
   </head>
   <body>
      <h1 id="myid">Sample Text</h1>

      <button onclick="getDataFromHtml()">Try Now!</button>
   </body>
</html>

Click the 'Try Now!' Button and See the Magic!

Sandler answered 24/3, 2019 at 13:18 Comment(0)
S
1

This post provides the solution how to pass parameter to a templated html:

html = HtmlService.createTemplateFromFile('page2');
html.id = s1;

Then in page 2 use the the tag to print out the id value;

<div class="info" >
    <span id="ID" name="ID"><?=id?></span>
</div>
Synchrocyclotron answered 18/6, 2019 at 3:5 Comment(1)
This solution is the official way I think, but it has a problem: when the template file is too large, apps script will return "Exceeded maximum stack depth" error.Spiritless

© 2022 - 2024 — McMap. All rights reserved.