7
votes

I've made a Google Apps Script deployed as a standalone web app using HTMLService that provides a simple front end to enter budget data into a Google Spreadsheet. I'm using JQuery Mobile for some of the javascript as well as to style it a mobile-friendly manner, as my main use case for this app is to enter purchases from my mobile.

My problem is that on a mobile browser, the app doesn't scale properly. It's the width of the browser, but it's as if it was "zoomed out". All the controls become essentially unusable on mobile.

If the script is embedded in a Google Site, it scales properly, but I'd rather be able to view the web app directly, rather than embed it in Google Sites.

EDIT: My rep is high enough to post photos now, so here they are (below code).

EDIT: The beginning of my HTML is below. I originally had the javascript and the full HTML in here, and I can add snippets if needed, but I reviewed it again and don't think it's relelvant to the problem it was cluttering up the question, so I removed it.

HTML:

<!DOCTYPE html>

<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css">
<?!= include('javascript'); ?>

<div data-role="page" data-theme="a" id="main">
        <div data-role="content">
            <form id="myForm">
...

Code.gs:

function doGet() {
    return HtmlService.createTemplateFromFile('index').evaluate()
    .setSandboxMode(HtmlService.SandboxMode.IFRAME).setTitle('Budget Entry');
}

Direct Access (left) and Embedded in Google Sites (right).

Snippet with full code:

//<script>  
  function formSuccess() {
    var dateSelect =    document.getElementById("date");
    var dateSelected =  dateSelect.options[dateSelect.selectedIndex].text;
    var catSelect =     document.getElementById("category");
    var catSelected =   catSelect.options[catSelect.selectedIndex].text;
    var amountEntered = document.getElementById("amount").value;
    var noteEntered =   document.getElementById("note").value;

    var successMsg = 'Date: ' + dateSelected + 
                '<br>Category: ' + catSelected + 
                '<br>Amount: $' + amountEntered + 
                '<br>Note: ' + noteEntered;
                
    $('#dialogMain').html(successMsg);
    $.mobile.silentScroll(0);
    $.mobile.changePage( "#dialog", { role: "dialog" } );
    
    requestCategoryInfo(document.getElementById("status"));
    document.getElementById("amount").value = '';
    document.getElementById("note").value = '';    
  }
  
  function submitForm() {
    if (document.getElementById('amount').value.length == 0) {
      alert('Please enter an amount.');
      return;
    }
    $.mobile.loading( 'show' );
    $('#status').html('');
    google.script.run
          .withSuccessHandler(formSuccess)
          .processForm(document.getElementById('myForm'));
  }
  
  function loadUI() {
    $.mobile.loading( 'show' );
    loadDateSelect();
    google.script.run.withSuccessHandler(loadCategoryNamesAndValues).withFailureHandler(sendLog)
      .getCategoryNamesAndValues();
    $.mobile.loading( 'hide' );
  }
  
  function loadDateSelect(){
    var d = new Date();
    var month = d.getMonth()+1;
    var today = d.getDate();
    var daysInAMonth = [0,31,28,31,30,31,30,31,31,30,31,30,31];
    
    for (var n=1; n <= daysInAMonth[month]; n++) {
      var option = $("<option>").attr('value',n).text(month+"/"+n);
      $('#date').append(option);
    }
    $('#date').val(today);
    $('#date').selectmenu('refresh', true);
  }
  
  function loadCategoryNamesAndValues(catNamesAndValues){
    var namesAndValues = catNamesAndValues;
    var optionHTML = '';
    var currentGroup = '';
    var catName = '';
    var catID = '';
    
    for (var i=0; i<namesAndValues.length; i++) {
      catName = namesAndValues[i][0];
      catID = namesAndValues[i][1];
      
      if (catID.toString() == "Group"){ // Handle Group Name
        
        if (currentGroup.length > 0) { // close previous optgroup tag
           optionHTML += "</optGroup>";
        } 
        
        // Open optGroup
        currentGroup = catName;
        optionHTML += "<optGroup label='" + currentGroup + "'>";
        
      } else if (isNaN(parseInt(catID)) || parseInt(catID) == 0){ //Do Nothing
      
      } else { // Create Option HTML as: <option value=namesAndValues[i][1]>namesAndValues[i][0]</option>
      
        optionHTML += "<option value='" + catID + "'>" + catName + "</option>";
      }
    }
    
    // Close current OptGroup
    optionHTML += "</optGroup>"
    
    document.getElementById('category').innerHTML = optionHTML;
    $('#category').selectmenu('refresh', true);
  }
  
  function categoryChanged() {
    setStatus('');
    requestCategoryInfo(document.getElementById('status'));
  }
  
  function requestCategoryInfo(container) {
    $.mobile.loading( 'show' );
    google.script.run
          .withSuccessHandler(displayCategoryInfo)
          .withFailureHandler(sendLog)
          .withUserObject(container)
          .getCategoryInfo(document.getElementById('category').value);
  }
  
  function displayCategoryInfo(categoryInfo, container){
    var spentStr = 'Spent $' + categoryInfo.actual.toFixed(2) + ' of $' + categoryInfo.budgeted.toFixed(2);
    var remainingStr = 'Remaining: $' + categoryInfo.remaining.toFixed(2);
    
    var statusDiv = container;
    if (statusDiv.innerHTML.length > 0){ statusDiv.innerHTML += '<br>'};
    statusDiv.innerHTML += spentStr + '<br>' + remainingStr;
    
    if (String(categoryInfo.fundAmount).length > 0) {
      var fundAmountStr = '';
      
      if (categoryInfo.remaining < 0) {
        fundAmountStr = (categoryInfo.fundAmount + categoryInfo.remaining).toFixed(2);
      } else {
        fundAmountStr = categoryInfo.fundAmount.toFixed(2);
      }
      
      statusDiv.innerHTML += '<br>Fund: $' + fundAmountStr;      
    }
    $.mobile.loading( 'hide' );
  }
  
  function setStatus(html){
    document.getElementById('status').innerHTML = html;
  }
  
  function appendStatus(html){
    setStatus(document.getElementById('status').innerHTML + '<br>' + html);
  }
  
  function sendLog(){
    google.script.run.sendLog();
  }
//</script>
<!DOCTYPE html>

<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css">
<?!= include('javascript'); ?>

<div data-role="page" data-theme="a" id="main">
        <div data-role="content">
            <form id="myForm">
            
            <div>Date</div>
            <div><select name="date" id="date"></select></div>
            
            <div>Category</div>
            <div><select name=category id="category" onchange="categoryChanged()" required></select></div>
            
            <div>Amount</div>
            <div><input type="text" name="amount" id="amount" required></div>
            
            <div>Note</div>
            <div><input type="text" name="note" id="note"></div>
            
            <div><input type="button" id="submit" value="Submit" onclick="submitForm()"/></div>
            
            </form>
            
            <!--<a href="#dialog" data-role="button" data-rel="dialog" data-transition="pop">Dialog</a>-->
        </div><!-- /content -->
 
        <div data-role="footer">                
            <div id="status"></div>
        </div><!-- /footer -->
</div><!-- /page -->


<div data-role="page" id="dialog" data-close-btn="none">
  <div data-role="header">
    <h1 id="dialogHeading">Success!</h1>
  </div>

  <div data-role="main" class="ui-content" id="dialogMain">
    <p>Text goes here.</p>
  </div>
  <div class="ui-grid-b">
	<div class="ui-block-a"></div>
	<div class="ui-block-b"><a href="#main" data-role="button" data-icon="check">OK</a></div>
	<div class="ui-block-c"></div>
   </div><!-- /grid-a -->

  <!--><div data-role="footer"></div>-->
</div> 

<script type="text/javascript">
$(loadUI);
</script>
4
What sandbox setting are you using? IFRAME / NATIVE? Have you tried different settings? - Alan Wells
Tried both NATIVE and IFRAME. The JQuery Mobile styling doesn't show up with NATIVE at all (Chrome Dev Tools report two errors in the console). So I'm using IFRAME (better for speed anyway), which has no console issues. - Jason Malmstadt
Edited the question above to add the Code.gs snippet that contains the HTMLService call and sets the sandbox mode. - Jason Malmstadt
Agree it was good to remove all the unnecessary code... but as it is, people need to craft their own app to try to help you. Could you post the minimal code necessary to reproduce the problem? (As it stands, the zoom workaround in the issue Bryan pointed out is your only real option, aside from viewing within Google Sites.) - Mogsdad

4 Answers

2
votes
var output = HtmlService.createHtmlOutput('<b>Hello, world!</b>');
output.addMetaTag('viewport', 'width=device-width, initial-scale=1');

this will help

1
votes

You can determine the size of the display via CSS Media Queries. For example, adding this to your CSS causes the form to display differently depending on the device's screen size:

@media only screen and (min-device-width: 413px) and (max-device-width: 415px) { /* iPhone 6+ */
  #main, #dialog {
   zoom: 3;
   background: red;
  }
}

@media only screen and (min-device-width: 374px) and (max-device-width: 376px) { /* iPhone6 Styles */
  #main, #dialog  {
   transform: scale(2);
    background: blue;
  }
}

@media only screen and (min-device-width: 359px) and (max-device-width: 361px) { /* iPhone6+ Alt Styles */
  #main, #dialog  {
   transform: scale(2);
    background: green;
  }
}

@media only screen and (min-device-width: 319px) and (max-device-width: 321px) { /* iPhone5 or less Styles */
  #main, #dialog  {
   transform: scale(2);
    background: grey;
  }
}

Using Chrome's device emulation, the form looked pretty good. (The red background is set by the above css.) But when I accessed the app from my real iPhone 6+, not all elements zoomed equally. (Submit button for example.) So there is likely some other specific css needed to further tailor the result.

screenshot screenshot

1
votes

I had this exact problem. All I was trying to do is test some jQuery mobile templates for a website without deploying them to either Google App Engine or Google Cloud Storage.

Google Drive no longer lets you serve HTML directly, so the app script is the next best option.

The problem is that app scripts iframe everything, creating viewport problems for things meant to be viewed on mobile (even after fixing the problem where jQuery src= need to be https instead of http).

The fix is to have a META tag on the iframe page in addition to the HTML you're serving.

Anyway, the two answers that say to add a META tag worked great.

If you are serving a jQuery mobile page, this code.gs code worked for me:

function doGet() {
  var output = HtmlService.createHtmlOutputFromFile('test');
  output.addMetaTag('viewport', 'width=device-width, initial-scale=1');
  return output;
}

Where test is your test.html file.

0
votes

This should help addmetatagname() of Class HtmlOutput. You should modify the meta tag through code.