Home

wq/app.js

wq version: 1.0 1.1 1.2-beta
Docs > wq.app: Modules

Note: This documentation is for wq 1.2, which has not yet been finalized.

@wq/app

@wq/app

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

Overview

@wq/app is the highest-level wq.app module, and brings together a number of lower-level modules and wq conventions into an integrated API. The specific concepts leveraged by @wq/app include:

Installation

@wq/app is available via a PyPI package (wq.app) as well as an npm package (@wq/app). If you are not sure which one to use, use the PyPI package to get started. It is possible to later convert a PyPI wq.app project into a npm @wq/app project (with a few changes).

wq.app for PyPI

@wq/app for npm

python3 -m venv venv      # create virtual env (if needed)
. venv/bin/activate       # activate virtual env
python3 -m pip install wq # install wq framework (wq.app, wq.db, wq.start, etc.)
# pip install wq.app      # install wq.app only
npm install @wq/app

Project Layout

When working with wq.app for PyPI, use wq.start to initialize your project. In particular, wq.start will automatically configure your RequireJS settings and link wq.app's bundled JavaScript assets into your folder. When using @wq/app for npm, use create-react-app as a starting point and then install the @wq/app package.

wq.app for PyPI

@wq/app for npm

// wq.start will set up a project similar to this:

// 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.init(config).then(function() {
        app.jqmInit();
    });
});

// index.html
// <script src="js/lib/require.js" data-main="js/myapp"></script>
// Replace the contents of src/index.js with something like this:
import app from '@wq/app';
import config from './config';

app.init(config).then(function() {
    app.jqmInit();
});

// FIXME: Update public/index.html with a reference to wq.app's CSS

API

@wq/app is typically imported as app, though any local variable name can be used. The app module provides the following methods and properties.

Initialization

app.init(config)

The main required usage of @wq/app is to initialize it with app.init(). The function returns a Promise that will be resolved when the initialization is complete. After the app is configured, app.jqmInit() starts up jQuery Mobile and renders the initial screen. Finally, app.prefetchAll() preloads and caches data from all registered models (i.e. all entries in the pages configuration that have list set to true).

app.init(config).then(function() {
    app.jqmInit();
    app.prefetchAll();  // Optional
});

The available configuration options are shown below with their default values.

{
    // @wq/app options
    'debug': false,
    'jqmInit': false,
    'backgroundSync': true, // alternatively, noBackgroundSync: false
    'loadMissingAsHtml': true, // alternatively, loadMissingAsJson: false

    'pages': { /* ... */ }, // from wq config
    'transitions': { /* ... */} // see below

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

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

The configuration sections for the other core modules are passed on to the init() function for each module. In a few instances, @wq/app overrides the default settings for the respective modules. See the documentation for @wq/router, @wq/store, @wq/outbox, and @wq/template 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 and the other core modules. If specified as a number, debug will set the verbosity level in @wq/store.

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 not to make the user wait for forms to be submitted to the server, and instead to handle all @wq/store 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 setting noBackgroundSync to true. backgroundSync can also be enabled or disabled on a per-form basis by setting the data-wq-background-sync attribute. For example, the [login.html] provided by the wq Django template sets data-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 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 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 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 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 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.
maxwidth A shortcut for $.mobile.maxTransitionWidth. Defaults to 800 (note that vanilla jQuery Mobile defaults to false)
save Removed in wq.app 1.2

Creating a Configuration Module

The configuration object is typically defined as a module config.js that depends on the server-created wq config and a template fixture, then adds the additional attributes needed to initialize @wq/app.

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

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

return config;
});
import config from './data/config';
import templates from './data/templates';

// config.pages already exists on server-generated wq config

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

export default config;

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]_[mode]': "<html>...</html>",
     'partials': { ... }
}

Deprecated Options

Note: The login and logout jQuery events, as well as the top-level hooks postsave, saveerror, showOutboxErrors, presync, and postsync (which were to be defined as functions on the @wq/app config object), have all been removed and replaced with plugin hooks providing similar functionality. See below for more information.

<form> Handler

app.init() registers a custom submit handler that takes normal form POSTs and converts them to @wq/outbox 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 as Blob instances. See the @wq/app:photos documentation below 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 functionality.

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

By default @wq/app's 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">

Other Methods

app.use(plugin)

Register a plugin to customize @wq/app functionality. See Plugins below.

app.go()

As of wq.app 1.2 app.go() has been removed. Use app.nav() instead.

app.nav(path)

Trigger a route change to the specified path, which should not include the application base URL.

app.nav('items/1');

app.sync()

As of wq.app 1.2, app.sync() has been removed. Use app.retryAll() instead.

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 configuration object (see above) 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 contexts][template] as {{app_config}}, while app.wq_config is provided as {{wq_config}}.

app.models

After initialization, app.models will contain a @wq/model 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}}.

Plugins

@wq/app provides a simple plugin API to facilitate the incorporation of arbitrary custom functionality beyond the built-in page rendering and offline storage APIs. By using a central plugin API, @wq/app can ensure that any custom code you incorporate is executed at the right time, no matter whether the referenced page is the first one to load or is loaded via AJAX during jQuery Mobile navigation events. Most plugin hooks even work the same whether a page is rendered on the client or on the server.

A plugin is defined as a simple object with one or more of the hooks below. Plugins should always be registered via app.use(plugin) before calling app.init().

Plugin Hooks

name provided by purpose
name Uniquely identify the plugin. The init and reducer hooks require a name to function properly; it is optional otherwise.
init(config) @wq/app Initialize the plugin during @wq/app initialization. If the plugin is named "example", app.config.example will be passed to this function.
context(ctx, routeInfo) @wq/router Add information to the context before rendering.
run($page, routeInfo) @wq/router Customize UI (with jQuery) after the page is rendered and shown
ajax(url, data, method, headers) @wq/store Override how requests are submitted and/or responses are parsed
onsync(item) @wq/outbox Process outbox item just after it is synced to server
postsaveurl(item, alreadySynced) @wq/app Generate a custom URL to navigate to after form submission. Note that in many cases, the postsave page configuration option can be used instead.
actions @wq/store Define Redux action creators
thunks @wq/router Define asynchronous tasks in response to Redux actions
reducer(pluginState, action) @wq/store Update plugin state based on Redux actions
render(state) @wq/router Render global state changes (outside of the page rendering pipeline)

Note: The following hooks and events were removed in wq.app 1.2:

name type suggested migration path
onsave(item, result) Plugin Hook Use an onsync() hook instead. The server result will be available as item.result.
saveerror(item, reason, $form) Config Hook Use an onsync() hook instead. The error will be available as item.error.
showOutboxErrors() Config Hook Use an onsync() and/or run() plugin hook instead.
postsave() Config Hook Use a postsaveurl() plugin hook instead.
presync() / postsync() Config Hook Use the template context as needed for UI customizations. Pages displaying outbox contents are automatically re-rendered after each sync.
"login", "logout" jQuery events Use the template context as needed for UI customizations. As of wq.app 1.2, all pages (including server-rendered pages) are automatically re-rendered on the client if the login state changes.

Available Plugins

wq.app comes with a number of predefined plugins for common use cases. The most essential plugin is probably @wq/map, which seamlessly integrates configurable Leaflet maps into the @wq/app page rendering flow. The full list of included plugins is below. The source code for each plugin should be useful as a reference for creating your own custom plugin. In particular, note the use of the name, init(), context(), and run() properties on each module.

Module Description
@wq/app:spinner Start and stop jQuery Mobile spinner
@wq/app:patterns Support for nested forms
@wq/app:photos Helpers for requesting and displaying user photos on a mobile device
@wq/chart Configurable d3-based reusable charts, including time series and boxplots
@wq/map Leaflet integration for displaying and editing geographic information via GeoJSON
@wq/map:locate Utilities for requesting the user's location
@wq/markdown Markdown (marked.js) and code syntax highlighting (highlight.js) integration

Defining a Custom Plugin

Plugins are typically defined separate JavaScript files and imported. app.use() registers the plugin and any provided hooks.

wq.app for PyPI

@wq/app for npm

// myapp/myplugin.js
define({
    // Name and init are optional if no config is needed
    'name': 'myPlugin',
    'init': function(config) {
        this.config = config;
        // Note: @wq/app is available as this.app
    },

    // Customize context before rendering page
    'context': function(context, routeInfo) {
        // ...
     }
});

// myapp/main.js
define(['wq/app', './myplugin', './config'],
function(app, myPlugin, config) {

app.use(myPlugin);
app.init(config).then(function() {
    app.jqmInit();
    app.prefetchAll();
});

});
// src/myplugin.js
export default {
    // Name and init are optional if no config is needed
    name: 'myPlugin',
    init(config) {
        this.config = config;
        // Note: @wq/app is available as this.app
    },

    // Customize context before rendering page
    context(context, routeInfo) {
        // ...
    }
};

// src/index.js
import app from '@wq/app';
import myPlugin from './myplugin';
import config from './config';

app.use(myPlugin);
app.init(config).then(function() {
    app.jqmInit();
    app.prefetchAll();
});

patterns plugin

@wq/app:patterns

@wq/app:patterns is a @wq/app plugin providing support for nested forms.

wq.app for PyPI

@wq/app for npm

define(['wq/app', 'wq/patterns', './config'],
function(app, patterns, config) {

app.use(patterns);
app.init(config).then(...);

});
import app, { patterns } from '@wq/app';

app.use(patterns);
app.init(config).then(...);

FIXME: WIP

photos plugin

@wq/app:photos

@wq/app:photos is a [@wq/app plugin] that integrates with the PhoneGap Camera API and shows previews for user-selected photos. Together with the file processing functions in @wq/app and @wq/outbox, @wq/app:photos provides a complete solution for allowing volunteers to capture and upload photos via your offline-capable web or mobile app. Captured photos are saved in an outbox (@wq/outbox) until they can be synchronized to the server.

The Species Tracker application provides a complete demonstration of the offline capabilities of @wq/app:photos.

API

@wq/app:photos does not require a global configuration, as it is configured via data-wq- attributes on the related form elements.

wq.app for PyPI

@wq/app for npm

define(['wq/app', 'wq/photos', './config'],
function(app, photos, config) {

app.use(photos);
app.init(config).then(...);
import app, { photos } from '@wq/app';

app.use(photos);
app.init(config).then(...);

To leverage @wq/app:photos in your template, create your form elements with data-wq- attributes. If you are using wq.start and the default project layout, these will be set automatically in the generated edit templates for models with image fields.

element attribute purpose
<input type=file> data-wq-preview Indicates the id of an <img> element to display a preview image in after a photo is selected.
<button> data-wq-action Indicates the function to call (take or pick) when the button is clicked
<button> data-wq-input The name of a hidden input to populate with the name of the captured photo. (The photo itself will be saved in offline storage).
<input type=hidden> data-wq-type Notifies @wq/app that the hidden element is intended to be interpreted as the name of a photo captured via @wq/app:photos. The element should typically have the data-wq-preview attribute set as well.

The take and pick actions are wrappers for PhoneGap/Cordova's camera.getPicture() API, meant to be used in hybrid apps where doesn't work (e.g. on older devices or broken Android implementations).

Below is an example template with the appropriate attributes set:

<img id=preview-image>

{{^native}}
<input type=file name=file data-wq-preview=preview-image>
{{/native}}

{{#native}}
<button data-wq-action=take data-wq-input=file>
  Take Picture
</button>
<button data-wq-action=pick data-wq-input=file>
  Choose Picture
</button>
<input id=filename type=hidden name=file
   data-wq-type=file data-wq-preview=preview-image>
{{/native}}

Note the use of the {{#native}} context flag which is set automatically by @wq/app. See the Species Tracker template for a working example.

Browser Compatibility Notes

@wq/app/photos, and the related file processing functions in @wq/app and @wq/outbox, rely heavily on two browser features: - Offline storage (see Browser Compatibility Notes for @wq/store) - Binary Blob support, including the ability to upload Blobs via AJAX.

All modern browsers, including Internet Explorer 10, Android 4.4, and later versions, have at least minimal blob support. For IE 9 and older desktop browsers, the preview functionality in @wq/app:photos will not work, but users should still be able to upload files. @wq/app will detect the lack of Blob support on these browsers and fall back to a normal form post when a <form> containing an <input type=file> is encountered. (wq.db's ModelViewSet includes built-in support for responding to forms posted in this way).