16

I have a Google Sheet that I'm using as a database of clients. There are quite a lot of variables (columns) that I need to store per entry, so I thought that it would be easier to view each entry if I could have a Sidebar display all of the values of the active row.

Example: If I click cell C1, it will display the values from C1, C2, C3 and C4 in the sidebar.

Is this possible? I've gotten as far as getting the Sidebar to display but I cannot figure out how to get a value from a cell and print it to the Sidebar.

Thank you in advance, I've reached a dead end with my (very) limited intellect!

Mogsdad
  • 44,709
  • 21
  • 151
  • 275
user2910809
  • 383
  • 2
  • 5
  • 12
  • There is no "on click" event handler for a spreadsheet, so there is no way to do what you want. You *can* get a script to run from an "edit" or a "change" to the sheet. You could have a button in the sidebar, that displayed the data that you want. But then, you'd need to constantly be clicking the button. – Alan Wells Jun 03 '15 at 20:12
  • 1
    You can do this by having the sidebar poll the document. See [this answer](http://stackoverflow.com/questions/24773177/how-to-poll-a-google-doc-from-an-add-on/24773178#24773178) for an example. – Mogsdad Jun 03 '15 at 21:19

2 Answers2

45

Something like this?

screenshot

This add-on uses the poller idea from How to poll a Google Doc from an add-on to call a server function getRecord(). That function grabs the row of data that is currently selected, and returns it to the showRecord() callback on the client (JavaScript) side, which handles the presentation in the sidebar.

It's not complete - watch for TODO comments. I wonder if this would be worth further developing and publishing?

Code.gs

/**
 * @OnlyCurrentDoc  Limits the script to only accessing the current spreadsheet.
 */

var SIDEBAR_TITLE = 'Record Viewer';

/**
 * Adds a custom menu with items to show the sidebar and dialog.
 *
 * @param {Object} e The event parameter for a simple onOpen trigger.
 */
function onOpen(e) {
  SpreadsheetApp.getUi()
      .createAddonMenu()
      .addItem('View records', 'showSidebar')
      .addToUi();
}

/**
 * Runs when the add-on is installed; calls onOpen() to ensure menu creation and
 * any other initializion work is done immediately.
 *
 * @param {Object} e The event parameter for a simple onInstall trigger.
 */
function onInstall(e) {
  onOpen(e);
}

/**
 * Opens a sidebar. The sidebar structure is described in the Sidebar.html
 * project file.
 */
function showSidebar() {
  var ui = HtmlService.createTemplateFromFile('Sidebar')
      .evaluate()
      .setSandboxMode(HtmlService.SandboxMode.IFRAME)
      .setTitle(SIDEBAR_TITLE);
  SpreadsheetApp.getUi().showSidebar(ui);
}


/**
 * Returns the active row.
 *
 * @return {Object[]} The headers & values of all cells in row.
 */
function getRecord() {
  // Retrieve and return the information requested by the sidebar.
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var headers = data[0];
  var rowNum = sheet.getActiveCell().getRow();
  if (rowNum > data.length) return [];
  var record = [];
  for (var col=0;col<headers.length;col++) {
    var cellval = data[rowNum-1][col];
    // Dates must be passed as strings - use a fixed format for now
    if (typeof cellval == "object") {
      cellval = Utilities.formatDate(cellval, Session.getScriptTimeZone() , "M/d/yyyy");
    }
    // TODO: Format all cell values using SheetConverter library
    record.push({ heading: headers[col],cellval:cellval });
  }
  return record;
}

Sidebar.html

<!-- Use a templated HTML printing scriptlet to import common stylesheet. -->
<?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>

<!-- Below is the HTML code that defines the sidebar element structure. -->
<div class="sidebar branding-below">
  <p>
  This sidebar displays all cells in a row, as a "record".
  </p>
  <!-- The div-table class is used to make a group of divs behave like a table. -->
  <div class="block div-table" id="sidebar-record-block">
  </div>
  <div class="block" id="sidebar-button-bar">
  </div>
  <div id="sidebar-status"></div>
</div>

<!-- Enter sidebar bottom-branding below. -->
<div class="sidebar bottom">
  <img alt="Add-on logo" class="logo" width="25"
      src="https://googledrive.com/host/0B0G1UdyJGrY6XzdjQWF4a1JYY1k/apps-script_2x.png">
  <span class="gray branding-text">Record Viewer by Mogsdad</span>
</div>

<!-- Use a templated HTML printing scriptlet to import JavaScript. -->
<?!= HtmlService.createHtmlOutputFromFile('SidebarJavaScript').getContent(); ?>

SidebarJavascript.html

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
  /**
   * Run initializations on sidebar load.
   */
  $(function() {
    // Assign handler functions to sidebar elements here, if needed.

    // Call the server here to retrieve any information needed to build
    // the dialog, if necessary.

    // Start polling for updates        
    poll();
  });

  /**
   * Poll a server-side function at the given interval, to have
   * results passed to a successHandler callback.
   *
   * https://stackoverflow.com/a/24773178/1677912
   *
   * @param {Number} interval   (optional) Time in ms between polls.
   *                            Default is 2s (2000ms)
   */
  function poll(interval) {
    interval = interval || 1000;
    setTimeout(function() {
      google.script.run
        .withSuccessHandler(showRecord)
        .withFailureHandler(
          function(msg, element) {
            showStatus(msg, $('#button-bar'));
            element.disabled = false;
          })
        .getRecord();
    }, interval);
  };

  /**
   * Callback function to display a "record", or row of the spreadsheet.
   *
   * @param {object[]}  Array of field headings & cell values
   */
  function showRecord(record) {
    if (record.length) {
      for (var i = 0; i < record.length; i++) {
        // build field name on the fly, formatted field-1234
        var str = '' + i;
        var fieldId = 'field-' + ('0000' + str).substring(str.length)

        // If this field # doesn't already exist on the page, create it
        if (!$('#'+fieldId).length) {
          var newField = $($.parseHTML('<div id="'+fieldId+'"></div>'));
          $('#sidebar-record-block').append(newField);
        }

        // Replace content of the field div with new record
        $('#'+fieldId).replaceWith('<div id="'+fieldId+'" class="div-table-row"></div>');
        $('#'+fieldId).append($('<div class="div-table-th">' + record[i].heading + '</div>'))
                      .append('<div class="div-table-td">' + record[i].cellval + '</div>');
      }
    }

    // TODO: hide any existing fields that are beyond the current record length

    //Setup the next poll
    poll();
  }

  /**
   * Displays the given status message in the sidebar.
   *
   * @param {String} msg The status message to display.
   * @param {String} classId The message type (class id) that the message
   *   should be displayed as.
   */
  function showStatus(msg, classId) {
    $('#sidebar-status').removeClass().html(msg);
    if (classId) {
      $('#sidebar-status').addClass(classId);
    }
  }

</script>

Stylesheet.html

<!-- This CSS package applies Google styling; it should always be included. -->
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">

<style>
label {
  font-weight: bold;
}

.branding-below {
  bottom: 54px;
  top: 0;
}

.branding-text {
  left: 7px;
  position: relative;
  top: 3px;
}

.logo {
  vertical-align: middle;
}

.width-100 {
  width: 100%;
  box-sizing: border-box;
  -webkit-box-sizing : border-box;‌
  -moz-box-sizing : border-box;
}

#sidebar-value-block,
#dialog-elements {
  background-color: #eee;
  border-color: #eee;
  border-width: 5px;
  border-style: solid;
}

#sidebar-button-bar,
#dialog-button-bar {
  margin-bottom: 10px;
}

.div-table{
  display:table;
  width:auto;
/*  background-color:#eee;
  border:1px solid  #666666;*/
  border-spacing:5px;
}
.div-table-row{
  display:table-row;
  width:auto;
  clear:both;
}
.div-table-td, .div-table-th {
  display:table-cell;         
  width:200px;         
  background-color:rgb(230, 230, 230);  
}
.div-table-th {
  /*float:left;*/
  font-weight: bold;
}
.div-table-td {
  /*float:right;*/
}
</style>
Community
  • 1
  • 1
Mogsdad
  • 44,709
  • 21
  • 151
  • 275
  • 1
    *I wonder if this would be worth further developing and publishing?* it is for sure, I love it ! This is really brilliant (as usual)... just one question : what do you mean when you say "*TODO: hide any existing fields that are beyond the current record length"* I don't see any issue. (btw, I wonder why this is not yet accepted and why I'm the only "upvote" ? – Serge insas Jun 04 '15 at 18:29
  • Pretty spot on, thank you! I've implemented it and it works well. It would be great if it would convert numbers longer than three digits into formatted numbers like _$1,899,999_. Thanks again. – user2910809 Jun 04 '15 at 18:55
  • @Sergeinsas , only because I had just seen it and was testing it out – I'm so grateful to Mogsdad , exactly what I was hoping for. – user2910809 Jun 04 '15 at 18:59
  • 2
    @Sergeinsas & userX (time to become a "real person") - Thanks for the comments! Formatting: hook up (my) [SheetConverter library](https://sites.google.com/site/scriptsexamples/custom-methods/sheetconverter), as the TODO says, and Bang! Serge, re hiding existing fields... try deleting a column, or switching to a sheet that has fewer occupied columns, and you'll observe left-overs. It might be fun to "crowd-source" further development of an add-on, using GitHub say. I'll check it out and get back to you! – Mogsdad Jun 04 '15 at 20:33
  • 1
    How about transforming this to a "record editor" ? In other words making it bidirectional... Would be nice, don't you think ? I think about this because I can view far more fields at once than in a normal spreadsheet view when having many columns, even on a HD screen. – Serge insas Jun 05 '15 at 05:20
  • 1
    @Sergeinsas Exactly what I'm thinking. Excel (and Lotus 1-2-3 before it) have this feature. Very do-able, starting from here. Would be nice to respect any validation rules, too. – Mogsdad Jun 05 '15 at 09:16
  • @Mogsdad - As five years have passed, I want to tell you I have been inspired by this answer and comments to try out my own TBA version below – Chris Glasier May 30 '19 at 02:09
  • @Sergeinsas - please see above comment – Chris Glasier May 30 '19 at 04:12
  • 1
    @Mogsdad - Taking you back six years :) This sidebar viewer is exactly what I was looking for. I have recreated all the files as per your detailed list above, but I get the following error: ScriptError: We're sorry, a server error occurred while reading from storage. Error code PERMISSION_DENIED. Any ideas? Anyone? – user13708028 Aug 01 '20 at 18:04
2

Or, five years later, something like this?

TBA in sidebar

This is more perhaps a development of Mogsdad's answer but still responds to the OP.

Data in the TBA is positioned by associations rather than coordinates – Trail, Branch and Aspect lists are read separately and strung together – Floor 1 > Guestroom 3 > Test 1: Dirty. (see TBA overview)

<<< Displays the data for one aspect in the spreadsheet; >>> TBA reruns for new selections in sheet.

I would like to share the code. May I ask what is the best way to do that? (It is very early days)

Chris Glasier
  • 791
  • 2
  • 10
  • 21