@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.
@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:
localForage
-backed cache of REST API responses (@wq/model), with an offline-capable outbox to sync form submissions back to the server (@wq/outbox).@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).
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
Whether using wq.app for Python or @wq/app for npm, we highly recommend using wq.start to initialize your project. In particular, wq.start will automatically configure your JavaScript build settings and link the proper imports for @wq/app. If you are not using wq.start, you can create a similar layout using the examples below.
// "wq start --without-npm" creates a layout 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>
// "wq start --with-npm" is similar to create-react-app, but
// replaces 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();
});
// public/index.html
// <link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/css/myapp.css" />
@wq/app
is typically imported as app
, though any local variable name can be used. The app module provides the following methods and properties.
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': { /* ... */ }
}
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.
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 boolean backgroundSync
flag controls the default behavior for @wq/outbox form submissions. When true
(the default), form submissions are synced in the background while the user is navigated to the next screen. When false
, forms remain on screen until the server responds. 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.
As of wq.app 1.2, the
noBackgroundSync
flag is no longer supported - instead, setbackgroundSync: false
. Also, settingbackgroundSync
to a numeric sync interval is no longer supported, as all retry logic is handled internally by Redux Offline. See @wq/outbox for more information.
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 routesThe 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 thepages
section in yourconfig.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 TransitionsConfiguration 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 |
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.
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;
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': { ... }
}
Note: The
login
andlogout
jQuery events, as well as the top-level hookspostsave
,saveerror
,showOutboxErrors
,presync
, andpostsync
(which were to be defined as functions on the @wq/appconfig
object), have all been removed and replaced with plugin hooks providing similar functionality. See below for more information.
<form>
Handlerapp.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">
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. Useapp.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. Useapp.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 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}}
.
@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()
.
For example, if you wanted to provide a {{date}}
variable for use in HTML templates, you could define the following plugin:
var datePlugin = {
context: function() {
return {'date': new Date().toDateString()}
}
}
app.use(datePlugin);
name | processed 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. |
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/map | Leaflet integration for displaying and editing geographic information via GeoJSON |
@wq/map:locate | Utilities for requesting the user's location |
Plugins are typically defined separate JavaScript files and imported. app.use()
registers the plugin and any provided hooks.
// 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 is a @wq/app plugin providing support for nested forms.
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(...);
This plugin provides logic for adding and removing copies of a nested form. To use it, the edit template should contain one or more <button>
s with data-wq-action
and data-wq-section
attributes. The data-wq-section
should match the name of the nested form and corresponding array template block ({{#section}}{{/section}}
).
data-wq-action | description |
---|---|
addattachment |
Add another nested form, by rendering a copy of the section template block. |
removeattachment |
Remove the current nested form. |
An addattachment
button is provided in the default edit templates for nested forms.
photos
plugin@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.
@wq/app:photos does not require a global configuration, as it is configured via data-wq- attributes on the related form elements.
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 <input type=file>
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.
Last modified on 2019-07-10 10:42 AM
Edit this Page |
Suggest Improvement
© 2013-2019 by S. Andrew Sheppard