Flow/Architecture/Templating
Flow (since the Flow/Epic Front-End rewrite of April 2014) uses Handlebars to render in JavaScript, and the lightncandy PHP implementation. This allows templates to be shared between front-end and back-end.
MediaWiki was in the middle of Requests for comment/HTML templating library when Flow was being developed, but the Flow team couldn't wait.
Using
[edit]The templates are in the handlebars
subdirectory.
For example, handlebars/flow_post_partial.handlebars:
{{#with revision}}
<div class="flow-post{{#if isModerated}} flow-post-moderated{{/if}}">
{{#with author}}
<span class="flow-author"><a href="{{links.contribs.url}}" title="{{links.contribs.title}}" class="mw-userlink flow-ui-tooltip-target">{{name}}</a> <span class="mw-usertoollinks">(<a href="{{links.talk.url}}" class="new flow-ui-tooltip-target" title="{{links.talk.title}}">{{l10n "Talk"}}</a>{{#if links.block}} | <a class="flow-ui-tooltip-target" href="{{links.block.url}}" title="{{links.block.title}}">{{l10n "block"}}</a>{{/if}})</span></span>
{{/with}}
<div class="flow-post-content">
{{html content}}
</div>
...
This
- sets the scope to the 'revision' key of the API response.
- outputs some HTML
- uses handlebars' limited logic to conditionally output a class if the key
isModerated
is true in this scope - sets the scope to the 'author' key within 'revision', in preparation of outputting the usual MediaWiki Username (talk | contribs) HTML.
- outputs the value of various bits within the response.revision.author structure.
- Uses the
l10n
helper function to output some message strings. - Then uses the
html
helper function (described below) to render the revision.content.
General template structure
[edit]In handlebars templates:
{{> template_name}}
includes in another template{{func_name}}
{{var_name(.varname2 ...)}}
outputs the string value of some key in the current "this" scope~
inside the curly braces eats whitespace, thus{{~var_name}}
eats all whitespace before the{{
and{{~var_name~}}
eats all trailing whitespace.{{#func_name}} ... {{/func_name}}
invokes a block function, like{{#if someboolean}} ... {{/if}}
above. Flow has its own block helper function, like{{#eachPost}} ... {{#eachPost}}
{{#-- Comment here --}}
for comments
Note that templates aren't JavaScript (or PHP). You can't have expressions in template parameters. For example, {{#if foo && bar}}
won't work. You have to supply appropriate values to the template, or in this case nest #if blocks.
In general one main template includes other templates, often in #if or #each or #eachPost blocks.
PHP
[edit]The templates are compiled into PHP in advance by make compile-lightncandy, and we check the compiled templates into git.
On your development server, set wgFlowServerCompileTemplates = true
to compile templates as needed (you may have to change permissions on handlebars/compiled
.
Helper functions
[edit]Handlebars helper functions have to be re-implemented in JavaScript (in modules/engine/misc/flow-handlebars.js) and PHP (includes/TemplateHelper.php) in order for a template to work both front-end and back-end.
In a template, {{hFuncName this extra_params}}
in a template invokes hFuncName( context, extra, params, options )
.
You can also call helpers with key-value parameters: {{hFuncName foo=1 bar="baz"}}
.
E.g. {{html content}}
invokes the html
helper function to render HTML (rather than escaping '<' and '>', etc.).
Another example, the template code
{{uuidTimestamp postId "time_ago"}}
invokes the uuidTimestamp
helper to get the timestamp from the UUID postId
and format it using the 'time_ago' i18n message.
It's fine to create a helper which in turn calls a template with extra parameters.
In JavaScript
[edit]In JS, you define the helper implementation FlowHandlebars.prototype.MyFunc = function ( params ) {...
, then call handlebars.registerHelper( 'helpername', FlowHandlebars.prototypeMyFunc )
to associate it with the helper helpername in Handlebars templates.
SafeString is for when you know you're outputting safe HTML, it's used by Template:Html helper.
Wiring up functions in JavaScript
[edit]mw.flow.FlowHandlebars.prototype.processTemplate( templateName, object }
will return a DocumentFragment of the named template rendered using the data in the provided object. It caches the compiled template, so there's no reason to get templates before rendering them.
mw.flow.FlowHandlebars.prototype.processTemplate(
'timestamp',
{
time_iso: "1984-04-01 4:20",
time_readable: "back in the eighties"
}
)
Note: Seems _tplcache[ name] duplicates Mantle's cache of compiled templates!?
Init
[edit]flow.js
runs mw.flow.initComponent()
for every component on the page.
It looks for data-flow-component="some_name"
tags in the page HTML, then instantiates that component's class
E.g. a mention of data-flow-component="board" on page will initialize the FlowBoardComponent JavaScript object.
flow-board.js has mw.flow.registerComponent( 'board', FlowBoardComponent );
That's how the data-flow-component "board" and class FlowBoardComponent are glued together.
The class extends FlowComponent
, does the usual OO dance of constructor, calling parent (FlowComponent), creating an object.
FlowBoardComponent is the only component.
Associating actions
[edit]Typical front-end interaction code locates some elements in the HTML with selectors and binds various actions to them. Flow has a better way:
- give flow-xxx classes to HTML elements in templates that indicate that something should be done
- a data-flow-yyy attribute on the same HTML element can give information, e.g. the handler to be called.
- declare the handler function in the appropriate handlers part of the FlowBoardComponent so flow.js can find it.
Load handlers
[edit]For example
- in HTML
class="flow-load-interactive" data-flow-load-handler="MyFunc"
- define the function
FlowBoardComponent.UI.events.loadHandlers.MyFunc
It will be invoked at load time for that element. Flow load handlers include topicElement, timestamp
Click handlers
[edit]Similarly, for click interactions
- in HTML
class='flow-click-interactive' data-flow-interactive-handler="MyFunc"
on elements - define the function
FlowBoardComponent.UI.events.interactiveHandlers.myFunc
and on click this will be called. Flow click handlers include cancelForm, topicCollapserToggle
The class name is only necessary for non-interactable elements. A, BUTTON, and INPUT can all have the data-flow-interactive-handler
alone.
General approach: bind on the root container rather than individual bits of HTML, let the event bubble up to this and then call the component function.
Example: add an interactive handler function for music, define in Handlers.xxx
add to some .html.handlebars
template class="flow-click-interactive" data-flow-interactive-handler="music"
API handlers
[edit]- in HTML code
data-flow-interactive-handler="apiRequest" data-flow-api-handler="myCallbackFunc
- in JavaScript, add the callback for the API request as
FlowBoardComponent.UI.events.apiHandlers.myCallbackFunc
For example you would add this to some <a href=...
link,
The built-in handler apiRequest
intercepts the usual link/form target and submits the anchor or the form as an API call.
It parses out the URL and turns the GET params or the form parameters into api params and turns it into an API call.
There's a translation layer to translate from URL/Form paramerters to API parameters.
Your myCallbackFunc
function is called when the API call returns. Typically that would replace part of the page with the results of the API call, by rendering parts of the JSON response using templates.
The apiHandler callback handles both success and error state.
Questions
- How does Flow know what the API call is? It seems
flowApiRequestFromAnchor()
converts an existing static URL into an API call, but I don't see anything that calls this?
api pre-handler
[edit]Sends an overrideobject to the API to modify the request parameters.
If you need one of these you typically give it the same name as your api handler callback.
e.g. the activateEditHeader()
api handler has a matching api pre-handler which returns some API parameters for editing header that are not in the a href
URL parameters.
Another example: the preview()
API pre-handler changes the action
from the default flow
to flow-parsoid-utils
.
Register these with FlowBoardComponent.UI.events.apiPreHandlers
Can return false
to stop an API request from being made, e.g. form isn't ready.
Other
[edit]- data-flow-initial-state= collapsed / hidden
- sets initial state of a Flow form (such as textarea + Cancel Preview Reply) in a Flow board component to be collapsed or hidden
Debugging
[edit]Comment out a line in View.php to view the JSON output before the PHP template.
If you see [Object object] in output instead of the contents you expect, it's because the API return value was some complex object. Templates expected a string.
Best practices
[edit]Handlebars has implicit {{{ }}} triple-braces to output pre-computed HTML. This is easy to miss, so invoke an explicit {{ html keyName}}
when we want to output HTML