Client scripting

Client scripts run on users' computers, in their browsers' JavaScript runtimes. They're great for handling UI events and changing DOM elements and widget properties.

Client scripts can also trigger interactions with the server through App Maker APIs. For example, client scripts can fetch and modify data from a database or invoke server scripts.

Asynchronous operations and callback functions

Client scripts can run asynchronously in App Maker to keep your app's user interface (UI) responsive. In some browser apps, when an operation needs to wait on an external resource the entire app waits along with it. The UI waits, too, and is unresponsive.

With asynchronous code, the UI and other app operations stay responsive, but your scripts might not run in the order they're written. To run a script after an asynchronous operation finishes, such as code that handles the results of the operation, use a callback function.

Callback functions are client scripts that run when operations complete. You can specify functions that run when the operation is successful, or an object that runs different functions for success and failure scenarios.

Success-only callback functions

When you include a success-only callback function, the callback function executes only if the operation succeeds. If the operation fails, nothing happens. As a simple example, you can create a record asynchronously with createItem () and send an alert about it:

widget.datasource.createItem(function (record) {
  alert('Record with ID ' + record.id + ' was created in the database.');
});

Success and failure callback functions

An extension of the previous case, use this method to be notified of failures and to perform error reporting, clean up, or roll backs.

widget.datasource.createItem({
  success: function (record) {
    alert('Record with ID ' + record.id + ' was created in the database.');  // executes if record was created
  },
  failure: function (error) {
    console.info("No new record for you!"); // executes if record wasn't created
  }
});

No callback function

If you omit the callback or pass null as the callback, then the code continues without waiting for the operation to finish. For example:

    widget.datasource.createItem();
    console.info("warning, record is probably not yet created!");

Troubleshooting

Here are some strategies for troubleshooting your client scripts to find and fix errors:

  • Find syntax errors in the script editor. The App Maker script editor automatically warns you of syntax problems as you type. Check for warning labels to the left of your script's line numbers. The warnings catch common errors like forgotten parentheses.

  • Find runtime errors in your browser's JavaScript console. Open a preview instance or a deployed version of your app and open your browser's JavaScript console. In Chrome, open the JavaScript console by pressing either Ctrl+Shift+j or Alt-Cmd-j. Some common runtime exceptions are null dereferencing or uncaught throw statements. You can add debugger; statements to your script to stop script execution and identify the problem area (DevTools must be open for debugger; statements to take effect). For more debugging instructions, see Get started with debugging JavaScript in Chrome DevTools.

  • Follow your script's execution with logs and alerts. Use the browser's built-in console.log() function to send logs to the JavaScript console. Alternatively, use alert() to open an alert box that interrupts the execution of your script until you dismiss it.

Client script examples

Use scripts in binding expressions

You can use JavaScript to perform calculations in binding expressions. For example, the following expression in a button's enabled property disables the button if the Name field is blank, or the Age field has a value less than 18:

(@widget.parent.children.NameTextBox.value).length != 0 &&
@widget.parent.children.AgeTextBox.value >= 18

The App Maker parser first resolves the binding expressions, which start with @, to find the value of Name and Age. It then substitutes those values for the expressions, and evaluates the rest of the JavaScript.

The parentheses in the first line indicate the end of the binding expression. Without them, App Maker would watch the length property for changes, instead of the value property.

Call a server script

Calls to server scripts are asynchronous operations, so you have to use a callback to process the result of the script.

The Forum sample app includes an example of a client script that calls a server script. In this app on the View page, a client script runs when the user clicks the Submit button. This script confirms that App Maker creates a record and runs a client function, notifyForumOwnerAboutNewMessageClient, that runs a server script, notifyForumOwnerAboutNewMessageServer.

The following code is modified from the Forum sample app for clarity. To trigger the client script when the user clicks the Submit button, the onClick event is a custom action:

handleSubmitButtonClick(widget)

This action calls the handleSubmitButtonClick function in the following client script, which then calls the notifyForumOwnerAboutNewMessageClient function. The notifyForumOwnerAboutNewMessageClient function calls the notifyForumOwnerAboutNewMessageServer function in a server script:

function notifyForumOwnerAboutNewMessageClient(forumKey, messageKey) {
  google.script.run
    .withSuccessHandler(function(result) {
      console.log('Email sent');
    })
    .withFailureHandler(function(error) {
      console.log('Email not sent ' + error.message);
    })
    .notifyForumOwnerAboutNewMessageServer(forumKey, messageKey);
}

function handleSubmitButtonClick(widget) {
  widget.datasource.createItem(function(createdRecord) {
    var forumKey = createdRecord.Forum._key;
    var messageKey = createdRecord._key;
    notifyForumOwnerAboutNewMessageClient(forumKey, messageKey);
  });
}

The following server script checks if the forum owner is subscribed to the forum then notifies the subscribed owner that the forum has a new post:

function notifyForumOwnerAboutNewMessageServer(forumKey, messageKey) {
  var forum = app.models.Forum.getRecord(forumKey);
  if (forum.OwnerSubscribed) {
    var message = app.models.Message.getRecord(messageKey);

    try {
      MailApp.sendEmail({
        to: forum.Owner,
        subject: 'New message in your forum: "' + forum.Title +
          '" by ' + message.Author,
        htmlBody: message.Text
      });
    }
    catch (e) {
      console.log('Sending email notification for forumKey="%s" ' +
          'and messageKey="%s" failed.', forumKey, messageKey);
      console.log(e.message + '-> ' + e.stack);
    }
  }
}

Dynamically load external scripts

You can load many third-party scripts synchronously with External JavaScript Libraries, but some require a little more work. Libraries that require a callback parameter, like the Google APIs Client Library need to be loaded dynamically, by adding a script to Settings > General > onAppStart.

The following script loads the Google+ library:

// Suspends app loading until after the Google Client API loads.
loader.suspendLoad();
// Defines a callback function, for the client API. It must be global,
// so it's explicitly attached to the window object.
window.OnGapiClientLoad = function() {
  // Uses the Google Client API to load the Google+ library.
  gapi.client.load("plus", "v1", function() {
    // Continues loading the app once Google+ loads.
    loader.resumeLoad();
  });
};

var script = document.createElement("script");
script.setAttribute("type", "text/javascript");

// Specifies the name of the callback function in the "onload"
// parameter of the URL.
var url = "https://apis.google.com/js/client.js?onload=OnGapiClientLoad";
script.setAttribute("src", url);

document.getElementsByTagName("head")[0].appendChild(script);

Call Google APIs

This final example is a little more in-depth, and gives you a feel for what an actual App Maker script looks like. It makes use of a Google JavaScript API.

The script displays a Google Map in a page and updates it based on user input. The script works with a page named Map that contains the following widgets:

  • Two text fields, named Street and Zip.
  • A 200x200 HtmlArea named MapDiv which has an onAttach event that calls loadMaps()
  • A button, with an onClick() handler that calls updateMap()

Script:

var map;
var geocoder;
var marker;

// Called by loadMaps() to set up widgets.
function createMap() {
  var div = app.pages.Map.descendants.MapDiv.getElement();
  map = new google.maps.Map(div, {
    center: new google.maps.LatLng(-34.397, 150.644),
    mapTypeId: google.maps.MapTypeId.ROADMAP,
    zoom: 10,
  });
  geocoder = new google.maps.Geocoder();
  marker = new google.maps.Marker({map: map});
}

// Returns an address string from Street and Zip text fields.
function getAddress() {
  var page = app.pages.Map;
  var street = page.descendants.Street.value;
  var zip = page.descendants.Zip.value;
  return street + ", " + zip;
}

// Positions the map at the given location coordinates.
function showLocation(locations, status) {
  if (locations.length > 0) {
    var latLng = locations[0].geometry.location;
    map.panTo(latLng);
    marker.setPosition(latLng);
  }
}

// Sets up the Google Maps library.
function loadMaps() {
  google.load("maps", "3", {callback: createMap});
}

function updateMap() {
  geocoder.geocode({address: getAddress()}, showLocation);
}

Script highlights:

  • MapDiv uses its onAttach() handler to set up the Google Maps API.
  • The script uses getAddress() to access user input by pulling data from the Street and Zip fields.