-
Notifications
You must be signed in to change notification settings - Fork 5
Page Building
Pages start their life in navigation.js
. navigation.js
uses match_route
to determine which view to use, then uses views.build
to start the builder in motion.
A view is a JS file that lives in src/media/js/views/
and exposes a function accepting one argument:
define('views/myview', ['l10n'], function(l10n) {
var gettext = l10n.gettext; // Trust me, do this.
return function(builder) {
// ...
};
});
In the space before the return
, you can assign global event handlers. This is where you'll handle any events that the page will throw. The returned function is what gets called by the views
module and starts the page construction process.
Templates that are rendered with the builder (and only the builder) are able to use the {% defer %}
block extension. This is the tool which enables magic state management for a page.
This is the most basic example:
{% defer (url=api('foo')) %}
I just loaded {{ this }}.
{% end %}
In this example, the defer block asynchronously loads the content at api('foo')
. When it has loaded, the content within the defer block is inserted into the page's template. this
contains the content returned form the server.
If two defer blocks have the same url
parameter, the request will only be made once. This is seamless to the developer, however.
Multiple arguments are available on the defer block to change its behavior.
This argument extracts a value from the returned response and reassigns the value to this
. The following two code snippets are equivalent:
{% defer (url=api('foo')) %}
I just loaded {{ this.name }}.
{% end %}
{% defer (url=api('foo'), pluck='name') %}
I just loaded {{ this }}.
{% end %}
While the pluck
argument isn't obviously useful, it becomes necessary to facilitate model caching.
When pluck
is used, the original value of this
is preserved in the variable response
.
This argument notes that the content returned in this
matches the format of the specified model. For instance:
{% defer (url=api('foo'), as='app') %}
I just loaded {{ this.name }}.
{% end %}
This flags the returned object as an "app" object. The object will get cached and returned when an app with similar properties is requested.
If this
is a list, each of its elements is cast as the specified model. For instance, if the server's response was:
[
{"foo": 123},
{"foo": 456}
]
The following code would cast each of the elements in the list as "foomodel"s.
{% defer (url=api('foo'), as='foomodel') %}
{% for item in this %}
{{ item.foo }}
{% endfor %}
{% end %}
This argument must be paired with the as
argument.
This argument implies that the value that's expected to be returned by the server will be in the format of the model described by as
and will be stored under the specified key. For instance, the following snippet asserts that the data returned by the server is an app with the slug "zipzap"
.
{% defer (url=api('foo'), as='app', key="zipzap") %}
I just loaded {{ this.name }}.
{% end %}
If the app already exists in the model cache, it is retrieved directly without making a request to the server. This follows the same caching rules as full-on HTTP caching.
This argument assigns an ID to the defer block. This ID can be used to retrieve the result of the request made for the block:
builder.results['the_id'] // Contains the result of the query, even if cached
It can also be used to establish an event handler for the block's completion:
builder.onload('the_id', function() {/* ... */});
This argument facilitates continuous pagination. Specify the selector for the wrapper of your paginatable content (e.g.: ul.my_list
) here.
When an element matching the selector .loadmore button
inside your template is clicked, its data-url
attribute will be read and the defer block will re-run with that URL as the API URL. The template is re-rendered with the new (next page) content, but is not pushed to the DOM. Instead, the HTML is parsed and the element matching the selector provided to the paginate
attribute is extracted. Its contents are then appended to the end of the corresponding element currently in the DOM. Here's a great example:
{% defer (url=api('foo'), paginate='ul.the_list') %}
<ul class="the_list">
{% for result in this %}
<li>{{ this.bar }}</li>
{% endfor %}
</ul>
<div class="load_more">
<button data-url="{{ api('next page') }}">Load More!</button>
</div>
{% end %}
There's enough magic here that doing what you think is right will probably get you the correct result.
If pagination fails (i.e.: if the request were made otherwise, the {% except %}
block would have kicked in), a fallback template will be loaded in lieu of the content specified by paginate
. The template is located in the setting settings.pagination_error_template
. The error template's context is passed the variable more_url
, which is the URL that was originally requested (and failed). This is useful for creating a "try again" button.
Defer blocks are designed with the idea that the base template is rendered immediately and content asynchronously loads in over time. This means that some placeholder content or loading text is often necessary. The placeholder extension to the defer block facilitates this.
{% defer (url=api('foo')) %}
I just loaded {{ this.name }}.
{% placeholder %}
This is what you see while the API is working away.
{% end %}
Placeholder blocks have access to the full context of the page.
If the response to the URL of the defer block is cached or the object being requested (with a key
argument) is already stored locally, the placeholder block is never rendered. Instead, the defer content is rendered directly during the initial build process.
If the value of this
(i.e.: the plucked value) would otherwise be an empty list, an alternative {% empty %}
extension is rendered. This is useful for lists where the amount of content is unknown in advance.
{% defer (url=api('foo')) %}
<ul>
{% for result in this %}
<li>I just loaded {{ result.slug }}.</li>
{% endfor %}
</ul>
{% placeholder %}
This is what you see while the API is working away.
{% empty %}
<div class="empty">
There's nothing here.
</div>
{% end %}
Note that if an empty is not defined, the defer block is simply run with the empty list as the this
variable.
If the request to the server leads to a non-200 response (after redirects), an optional {% except %}
extension block will be rendered instead.
{% defer (url=api('foo')) %}
I just loaded {{ this.name }}.
{% except %}
Oh no! There was a problem and the content wasn't loaded.
{% end %}
The variable error
is defined in the context of an except block. This variable contains an HTTP response code (numeric) for the failure, or a falsey value if there was a non-HTTP error.
A default except
template is rendered if one is not specified. The template used is listed in settings.fragment_error_template
.
Here's the simplest possible view:
return function(builder) {
builder.start('template_name.html');
builder.z('title', gettext('Page title'));
builder.z('type', 'leaf');
};
This function starts the build process by rendering a base template to the page. The first argument is the path to the compiled template. A second optional argument is accepted which provides extra context variables to the template.
builder.start('foo.html', {slug: 'the app slug'});
This function sets app context values. It accepts two arguments: a key and a value. Any values are accepted and stored in z.context
. Certain values have special behaviors:
builder.z('title', gettext('The Title')); // Sets the page title and `document.title`
builder.z('type', 'search'); // Sets `data-page-type` on the body
This defines a callback for a defer block with an ID. Consider the following:
{% defer (url=api('foo'), id='the_id') %}
Everyone cares about when this loads.
{% end %}
builder.onload('the_id', function() {
// ...
});
Note that these callbacks, if the defer block's URL or model were cached, may fire synchronously rather than asynchronously. They should always be placed at the end of the view function.
These methods are chainable.
While the builder object is oftentimes not accessible, if a reference to it is available and the current viewport is changing pages, calling builder.abort
will terminate all outstanding HTTP requests and begin cleanup.
This should never be called from an onload
handler.
The builder object itself is a promise object, meaning that .done()
, .fail()
, and other Deferred methods are available. These represent the completion state of the page as a whole.
As a matter of principle, these should not be used for setting event handlers or modifying content on the page. Instead, you should prefer onload
callbacks, as those will happen when each defer block returns (making the view more reactive).