Home

wq/app.js

wq version: 0.7.8 1.0 1.1
Docs > wq.app: Modules

wq/app.js

wq/app.js

wq/app.js is a wq.app module that provides a configuration-driven JavaScript application controller to facilitate the creation of complete mobile web apps for viewing and submitting field data. wq/app.js is primarily intended for use as a client for wq.db.rest, but can be used with any REST service with the same URL structure.

Overview

wq/app.js is the highest-level wq.app module, and brings together a number of lower-level modules and wq conventions into an integrated API. As a primarily configuration-driven module, wq/app.js is relatively easy to use, but enforces a number of constraints on the design of your project. In particular, it works best with wq.db.rest or a compatible REST service.

The specific concepts leveraged by wq/app.js include:

API

wq/app.js is typically imported via AMD as app, though any local variable name can be used.

// myapp.js
define(['wq/app', ...], function(app, ...) {
    app.init(config);
});

The app module provides the following methods and properties.

Initialization

In a simple project, the only required usage of wq/app.js is to initialize it with a configuration. The configuration object is described in the Configuration section below. app.init() returns a Promise that will be resolved when the initialization is complete.

The version of jQuery Mobile included with wq.app is customized to disable automatic initialization on startup. The purpose of this change is to make it easier to register custom plugins and events before jQuery Mobile starts up, to ensure they are executed when it does. To start up jQuery mobile, call app.jqmInit() after calling app.init() and registering your custom routes. If you have no custom events, you can set config.jqmInit = true to have app.init() call app.jqmInit() for you.

While not required, it is often useful to call app.prefetchAll() after initialization. app.prefetchAll() preloads and caches data from all registered models (i.e. all entries in the pages configuration that have list set to true).

// No custom events, no prefetch
config.jqmInit = true;
app.init(config);

// Custom events via low-level router API, then prefetch all
app.init(config).then(function() {
    router.addRoute(...);
    app.jqmInit(); 
    app.prefetchAll();
});

Plugins

wq/app.js provides a simple plugin API and a number of predefined optional plugins such as wq/map.js and wq/chartapp.js. Plugins should always be registered via app.use(plugin) before calling app.init().

// map is a wq/app.js plugin
config.jqmInit = true;
app.use(map);
app.init(config);

While it is possible to register arbitrary jQuery Mobile event handlers and/or low-level routes using wq/router.js, the plugin API is the recommended way to add custom functionality to pages before and/or after they render.

<form> Handler

app.init() registers a custom submit handler that takes normal form POSTs and converts them to wq/outbox.js entries. For model-backed list pages, these forms would normally be placed in [page]_edit.html templates and accessed via /[page_url]/new and/or /[page_url]/[id]/edit .

In addition to the other standard form input types, the <form> handler supports saving photos and other files, which are stored in wq/outbox.js as Blob instances. See the wq/photos.js documentation for more information about this feature.

To avoid conflicts with jQuery Mobile's own AJAX form handler, it is recommended to set data-ajax="false" on all forms using the wq/outbox.js functionality.

<form method="post" action="/items" data-ajax="false">

By default wq/app.js' form handler is applied to every form in the website. This functionality can be disabled on a per-form basis by setting data-wq-json=false on the form tag:

<form method="post" action="/custom" data-wq-json="false">

To disable both jQuery Mobile's AJAX form handler and wq/app.js' AJAX+JSON form handlers, specify both properties:

<form method="post" action="/custom" data-wq-json="false" data-ajax="false">

When working with list pages, the same template ([page]_edit.html) is used for both new and existing items. The presence of an id attribute in the context object can be used to distinguish between the two use cases when rendering the template. In addition, a number of other useful context variables are provided to assist with rendering e.g. <select> menus for foreign keys. See the template context documentation for more details.

<!-- Different action/method depending on whether this is a new or existing item -->
<form action="/items/{{id}}" method="post" data-ajax="false">
  {{#id}}
  <!-- Existing item; override HTTP method for Django REST Framework -->
  <input type="hidden" name="_method" value="PUT">
  {{/id}}

app.go()

app.go() is called automatically whenever the URL changes, e.g. in response to the user clicking on a link. However, there are rare cases where you may need to call it directly. app.go() is essentially a wrapper for router.go() that automatically generates the appropriate template context for list, detail, edit, and simple views.

app.go(page, ui, params, [itemid], [edit], [url], [context])

app.go() accepts up to seven arguments:

name purpose
page The name of a page listed in config.pages (described below)
ui A jQuery Mobile ui object describing options for an event
params Any additional URL parameters (?name1=value1&name2=value2 etc.)
itemid (optional) The id for an individual item in a list (triggers a detail view instead of a list view)
edit (optional) Boolean, if true triggers an edit view instead of a detail view
url (optional) URL to display for the rendered page. Usually this is automatically computed.
context (optional) Initial template context variable (will be merged with the automatically generated context).

The default template contexts for detail and edit views contain a number of context variables that assist in looking up and displaying information from related models, e.g. a list of potential values for a foreign key. These lookup contexts can be customized by overriding hooks (see Advanced Customization, below).

app.sync()

Triggers a background sync of pending outbox items in wq/outbox.js. Called automatically at regular intervals if backgroundSync is enabled (see Configuration below). Outbox items causing server errors will be excluded from further syncs after 3 sync attempts (This threshold can be controlled via wq/outbox.js' maxRetries option.) To try re-sending all unsaved items, including those causing server errors (for example in response to a user-initiated sync), call app.sync(true) instead of app.sync().

app.user

If the application supports authentication and the user is logged in, app.user will be set with information about the current user provided by the server. This information will also be available in the template context, e.g. {{#is_authenticated}}{{user.username}}{{/is_authenticated}}.

app.config, app.wq_config

A copy of the wq/app.js configuration object (see below) and the wq configuration object, respectively. Initially app.config.pages and app.wq_config.pages are the same, but after logging in, app.wq_config is overwritten with an updated wq configuration object with permissions information specific to the logged-in user. app.config is made available in the template context as {{app_config}}, while app.wq_config is provided as {{wq_config}}.

app.models

After initialization, app.models will contain a wq/model.js instances for each registered model (i.e. each item in the pages configuration with list: true).

app.models.item.filter({'type_id': 2}).then(function(type2items) {
    type2items.forEach(function(item) {
        console.log(item.id + ' - ' + item.label);
    });
});

app.native

Whether the application is running under PhoneGap / Cordova or as a web app (true and false respectively). Available in the template context as {{native}}.

Configuration

As noted above, wq/app.js is primarily configuration driven. The available configuration options are shown below with their default values.

{
    // wq/app.js options
    'debug': false,
    'jqmInit': false, // See init() above
    'backgroundSync': true, // alternatively, noBackgroundSync: false
    'loadMissingAsHtml': true, // alternatively, loadMissingAsJson: false
    'pages': { /* ... */ }, // from wq config
    'transitions': { /* ... */ },
    'hookName': function() {} // See hook names below

    // Configuration for core modules
    'router': { /* ... */ },
    'store': { /* ... */ },
    'outbox': { /* ... */ },
    'template': { /* ... */ },

    // Configuration for registered plugins
    'map': { /* ... */ },
    'chartapp': {  /* ... */}
}

The configuration sections for the other core modules are passed on to the init() function for each module. In a few instances, wq/app.js overrides the default settings for the respective modules. See the documentation for wq/router.js, wq/store.js, wq/outbox.js, and wq/template.js for more information about the available configuration options for each module. Similarly, any registered plugins can be configured via sections with the same name as the respective plugins.

Scalar Options

The debug option enables console logging in wq/app.js and the other core modules. If specified as a number, debug will set the verbosity level in wq/store.js.

The jqmInit option tells app.init() to immediately trigger app.jqmInit() on startup. The default is false, to give you a chance to register additional custom routes before initializing jQuery Mobile.

The backgroundSync option tells wq/app.js not to make the user wait for forms to be submitted to the server, and instead to handle all wq/store.js syncing in the background. If specified as a number, backgroundSync will set the number of seconds between sync attempts. Setting backgroundSync to true (the default) will trigger 30 seconds between sync attempts. It can be disabled by settingnoBackgroundSynctotrue.backgroundSynccan also be enabled or disabled on a per-form basis by setting thedata-wq-background-syncattribute. For example, the [login.html] provided by the wq Django template setsdata-wq-background-sync` to false since it makes more sense to wait for a successful login before continuing.

The loadMissingAsHtml and loadMissingAsJson options tell wq/app.js what to do if the user navigates to a model instance that is not stored locally. There are three possible outcomes in this case: * If the associated model page is configured with partial: false, wq/app.js will assume the entire model collection is stored locally, assume the page does not exist, and call the router.notFound() 404 page. * If the associated model page is configured with partial: true, and loadMissingAsHtml is set, wq/app.js will attempt to load the page from the server and assume the server is capable of rendering content as HTML. * If the associated model page is configured with partial: true, and loadMissingAsJson is set, wq/app.js will attempt to load the missing data from the server as JSON and render it locally.

pages: URL routes

The pages configuration section is equivalent to the option with the same name in the wq configuration object. The pages configuration is typically generated by the REST service and describes the URL routes in the application. The full list of page options is described in the documentation for the wq configuration object.

Note: If you need to customize an option in the server generated pages, you should specify it when calling router.register_model() in wq.db.rest rather than overriding the pages section in your config.js. This ensures that the client and the server are on the same "page".

As noted above, wq/model.js instances for all model-backed pages (those with list: true) will be added to app.models for convenience.

transitions: Page Transitions

Configuration for jQuery Mobile's built in page transitions. Where applicable, this information is mapped to jQuery Mobile's built-in configuration options.

Name Usage
default A shortcut for $.mobile.defaultPageTransition. Often set to slide.
dialog A shortcut for $.mobile.defaultDialogTransition.
save Sets the default transition to use when moving from an edit view back to a detail view after a save. This is often set to flip.
maxwidth A shortcut for $.mobile.maxTransitionWidth. Defaults to 800 (note that vanilla jQuery Mobile defaults to false),

Creating a Configuration Module

The configuration object can be defined as a variable in the same file as the call to app.init(). However, the conventional approach is to create a separate AMD module js/myapp/config.js that depends on the server-created wq config and then adds the additional attributes needed to initialize wq/app.js.

define(['data/config', 'data/templates', function(config, templates) {
// config.pages already exists on server-generated wq config

config.templates = {
    // Configure templates and default context variables
    templates: templates,
    partials: templates.partials,
    defaults: { /* ... */ }
}

config.transitions = {
    // Configure default jQuery Mobile page transitions
}
config.store = {
   // Configure wq/store.js
}
return config;
});

If you are not utilizing the wq.db-generated configuration, you can define the entire configuration object in config.js:

define({
    'pages': { /* ... */ },
    'template': { /* ... */ },
    'transitions': { /* ... */ },
    'store': { /* ... */ }
});

Inlining Templates

The templates can be created as regular Mustache HTML files and then inlined into a JavaScript object through the collectjson step in the wq build process. The resulting object should be of the form:

{
     '[modelname]_list': "<html>...</html>",
     '[modelname]_detail': "<html>...</html>",
     '[modelname]_edit': "<html>...</html>",
     // so on for each model...

     '[staticpage]': "<html>...</html>",
     'partials': {
          'head': "..."
     }
}

By convention, this object is typically wrapped in an AMD define() and placed in the file js/data/templates.js.

Putting it all Together

app.init() should typically be called immediately when the JavaScript loads. Assuming data/template.js and myapp/config.js are created as above, this can be accomplished by creating a main module for your application:

// js/myapp.js
requirejs.config({
    'baseUrl': 'lib', // wq and third party libs
    'paths': {
        'myapp': '../myapp',
        'data': '../data'
    }
});
requirejs(['myapp/main']);

// js/myapp/main.js
define(['wq/app', './config'],
function(app, config) {
   app.init(config);
});

And then configuring RequireJS to load it in your index.html:

<script src="js/lib/require.js" data-main="js/myapp"></script>

If you are utilizing both wq.app and wq.db in your project, you may find it useful to leverage the Django wq template which will set up the above project layout for you.

Advanced Customization

wq/app.js provides a number of events and hooks for additional customization.

Events

login

Callbacks registered with the login event will be called whenever the user logs in (as well as at the start of the application if the user is already logged in).

$('body').on('login', function(){
    /*...*/
});

logout

Callbacks registered with the logout event will be called whenever the user logs out.

$('body').on('logout', function() {
    /*...*/
});

Hooks

These hooks can be customized by adding additional options to the wq/app.js config object.

postsave(item, backgroundSync)

postsave() is called after a form submission with the item created by outbox.save(). It is primarily used to handle navigation to the next screen. postsave() can usually be configured as needed via the postsave property in the page configuration. The postsave() hook is meant to be overridden in cases where config.pages[name].postsave does not provide enough flexibility.

The backgroundSync argument indicates whether or not the form is being synced in the background, in which case the result of the submission might not be known yet. If backgroundSync is set, the default implementation of postsave() will navigate to the list view of the model that was just saved. If it's not set (i.e. result from the server is already known), postsave() will navigate to the detail view of the newly saved item by default.

config.postsave = function(item, backgroundSync) {
    if (backgroundSync) {
        app.go('outbox');
    } else if (item.synced) {
        $('#result').html("Successfully saved.");
    }
}
app.init(config);

saveerror(item, reason, $form)

If background sync is disabled, saveerror() is called when outbox.save() fails, with the item from outbox.save(), the reason for the failure, and the jQuery-wrapped <form> that initiated the save. The reason will be one of the constants app.OFFLINE, app.FAILURE, or app.ERROR. app.FAILURE usually indicates a server 500 failure, while app.ERROR usually indicates a 400 validation error.

If background sync is enabled (the default), then saveerror will not be used.

config.saveerror = function(item, reason, $form) {
    if (reason == app.OFFLINE) {
        alert("You are currently working offline.  Your record is in the outbox");
    } else {
        app.showOutboxErrors(item, $form);
    }
}
app.init(config);

showOutboxErrors(item, $form)

showOutboxErrors() is used to populate the screen with information about why a form was not successfully submitted. It is called by saveerror() by default, or when navigating to a previously saved outbox item.

The default implementation of showOutboxErrors() can automatically display any error messages returned from the REST API - both per-field errors and general validation errors. This functionality can be leveraged by placing <span> or <p> tags next to the fields and near the submit button. The tags should have two classes: error and [page]-[field]-errors, where page is the name of the of the wq page config and field is the name of the form field.

Example

<!-- item_edit.html -->

<form action="/items/{{id}}" method="post" data-ajax="false" data-wq-background-sync="false">
  {{#id}}
  <input type="hidden" name="_method" value="PUT">
  {{/id}}
  <ul>

    <li>
      <label for="item-name">Name</label>
      <input name="name" value="{{name}}" id="item-name">

      <!-- Placeholder for "name" errors -->
      <span class="error item-name-errors"></span>
    </li>

    <li>
      <label for="item-date">Date</label>
      <input name="date" type="date" value="{{date}}" id="item-date">

      <!-- Placeholder for "date" errors -->
      <span class="error item-date-errors"></span>
    </li>

    <li>
      <!-- Placeholder for general errors -->
      <span class="error item-errors"></span>

      <button type="submit">Save Changes</button>
    </li>
  </ul>
</form>

presync()

Triggered at the beginning of app.sync(). Useful for debugging.

postsync(result)

Triggered after an app.sync(), with the result from outbox.sendAll() in wq/outbox.js. Useful for alerting the user of persistent sync issues. The default implementation will check for and refresh any screens with data-wq-sync-refresh set to true. (This helps ensure that e.g. list views update automatically after a sync).

Low-Level APIs

wq/app.js relies on three low level modules that are discussed in depth elsewhere: